@savvy-web/mcp 2.7.4 → 3.0.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.
@@ -1,5 +1,8 @@
1
+ import { BiomeFailed, BiomeUnavailable, InvalidArgument, McpToolError, composeRemediatedMessage, invalidArgument, truncateEchoed } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { Lint } from "@savvy-web/silk-effects";
2
- import { Schema, SchemaGetter } from "effect";
5
+ import { Tool } from "effect/unstable/ai";
3
6
  import { spawnSync } from "node:child_process";
4
7
  import { realpathSync } from "node:fs";
5
8
  import { relative, resolve, sep } from "node:path";
@@ -208,29 +211,48 @@ const resolveContainmentRoot = (root, cwd, probe = gitWorktreeProbe) => {
208
211
  if (from.topLevel === home.topLevel) return null;
209
212
  return from.topLevel;
210
213
  };
214
+ /** Remediation for a path or cwd that escapes the containment tree. */
215
+ const CONTAINMENT_REMEDIATION = { hint: "Keep cwd and every path inside the server's workspace root, or inside a git worktree of the same repository." };
216
+ /** Remediation when no Biome binary can be located. */
217
+ const BIOME_REMEDIATION = { hint: "Install @biomejs/biome globally (recommended) or as a devDependency of the workspace." };
218
+ /** Remediation when Biome itself fails (not "lint issues found", which is a result). */
219
+ const BIOME_FAILED_REMEDIATION = { hint: "Biome did not complete; the message carries its stderr. Fix the configuration or invocation and retry." };
220
+ /** Build a {@link BiomeFailed} from a spawn error or a non-lint exit status. */
221
+ const biomeFailed = (raw, exitCode) => new BiomeFailed({
222
+ ...exitCode === void 0 ? {} : { exitCode },
223
+ message: composeRemediatedMessage(raw, BIOME_FAILED_REMEDIATION),
224
+ remediation: BIOME_FAILED_REMEDIATION
225
+ });
211
226
  /**
212
227
  * Run Biome and return structured diagnostics. When `write`/`unsafe` is set,
213
228
  * runs a fix pass first, then a read-only gitlab pass to report what remains.
214
229
  *
215
230
  * @remarks Resolves the Biome binary via {@link Lint.Biome.findBiome} (global
216
- * first, then the project's package manager). Throws if Biome is unavailable or
217
- * exits with status > 1 (Biome itself failed, vs. status 1 = lint issues found).
231
+ * first, then the project's package manager). Throws a typed
232
+ * {@link McpToolError} member {@link InvalidArgument} for a cwd/path outside
233
+ * the containment tree, {@link BiomeUnavailable} when no binary is found,
234
+ * {@link BiomeFailed} when Biome exits with status > 1 (Biome itself failed,
235
+ * vs. status 1 = lint issues found) — which {@link handleBiomeCheck} lifts
236
+ * into the Effect error channel unchanged.
218
237
  */
219
238
  const runBiomeCheck = async (args, fallbackCwd) => {
220
239
  const mode = args.mode ?? "check";
221
240
  const root = canonicalize(fallbackCwd);
222
241
  const cwd = canonicalize(args.cwd ?? fallbackCwd);
223
242
  const containmentRoot = resolveContainmentRoot(root, cwd);
224
- if (containmentRoot === null) throw new Error(`cwd escapes the workspace root: ${args.cwd}`);
243
+ if (containmentRoot === null) throw invalidArgument("cwd", `cwd escapes the workspace root: ${truncateEchoed(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
225
244
  const within = (abs) => abs === containmentRoot || abs.startsWith(`${containmentRoot}${sep}`);
226
245
  const paths = (args.paths && args.paths.length > 0 ? args.paths : ["."]).map((p) => {
227
246
  const lexical = resolve(cwd, p);
228
- if (!within(canonicalize(lexical))) throw new Error(`path escapes the workspace root: ${p}`);
247
+ if (!within(canonicalize(lexical))) throw invalidArgument("paths", `path escapes the workspace root: ${truncateEchoed(p)}.`, CONTAINMENT_REMEDIATION);
229
248
  return relative(cwd, lexical) || ".";
230
249
  });
231
250
  const doWrite = Boolean(args.write || args.unsafe);
232
251
  const biomeCmd = Lint.Biome.findBiome();
233
- if (!biomeCmd) throw new Error("Biome not found. Install @biomejs/biome globally (recommended) or as a devDependency.");
252
+ if (!biomeCmd) throw new BiomeUnavailable({
253
+ message: composeRemediatedMessage("Biome not found.", BIOME_REMEDIATION),
254
+ remediation: BIOME_REMEDIATION
255
+ });
234
256
  const parts = biomeCmd.split(" ");
235
257
  const bin = parts[0];
236
258
  const prefix = parts.slice(1);
@@ -254,8 +276,8 @@ const runBiomeCheck = async (args, fallbackCwd) => {
254
276
  timeout,
255
277
  killSignal
256
278
  });
257
- if (fix.error) throw fix.error;
258
- if ((fix.status ?? 0) > 1) throw new Error(`Biome --write failed (exit ${fix.status}): ${(fix.stderr ?? "").trim() || "unknown error"}`);
279
+ if (fix.error) throw biomeFailed(`Biome --write could not run: ${fix.error.message}`);
280
+ if ((fix.status ?? 0) > 1) throw biomeFailed(`Biome --write failed (exit ${fix.status}): ${(fix.stderr ?? "").trim() || "unknown error"}`, fix.status ?? void 0);
259
281
  wrote = true;
260
282
  }
261
283
  const readArgs = [
@@ -272,14 +294,45 @@ const runBiomeCheck = async (args, fallbackCwd) => {
272
294
  timeout,
273
295
  killSignal
274
296
  });
275
- if (read.error) throw read.error;
276
- if ((read.status ?? 0) > 1) throw new Error(`Biome failed (exit ${read.status}): ${(read.stderr ?? "").trim() || "unknown error"}`);
297
+ if (read.error) throw biomeFailed(`Biome could not run: ${read.error.message}`);
298
+ if ((read.status ?? 0) > 1) throw biomeFailed(`Biome failed (exit ${read.status}): ${(read.stderr ?? "").trim() || "unknown error"}`, read.status ?? void 0);
277
299
  return buildBiomeResult({
278
300
  diagnostics: parseBiomeGitlab(read.stdout ?? ""),
279
301
  wrote,
280
302
  ...args.strict !== void 0 ? { strict: args.strict } : {}
281
303
  });
282
304
  };
305
+ /** Wire parameters for `biome_check`. */
306
+ const BiomeCheckParams = Schema.Struct({
307
+ paths: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Paths to check. Defaults to the whole workspace." })),
308
+ mode: Schema.optionalKey(Schema.Literals(["check", "lint"]).annotate({ description: "check = lint+format+imports (default); lint = lint only." })),
309
+ write: Schema.optionalKey(Schema.Boolean.annotate({ description: "Apply safe fixes (--write)." })),
310
+ unsafe: Schema.optionalKey(Schema.Boolean.annotate({ description: "Apply unsafe fixes (--write --unsafe); implies write." })),
311
+ strict: Schema.optionalKey(Schema.Boolean.annotate({ description: "Report project warnings as errors (marked with originalSeverity). Default: honor project config." })),
312
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to run from. May be the server's workspace root, a directory inside it, or a git worktree of the SAME repository — a worktree contains the run to that worktree instead of the main checkout. Anything else is rejected." }))
313
+ });
314
+ /**
315
+ * The `biome_check` tool value. Mutating when `write`/`unsafe` is set, so it is
316
+ * annotated non-read-only and non-idempotent; `dependencies` is empty because
317
+ * the handler shells out directly and yields no service.
318
+ */
319
+ const biomeCheckTool = Tool.make("biome_check", {
320
+ 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 markdown in content[] and a typed object in structuredContent. NOTE: with write/unsafe this tool MUTATES files (git-reversible).",
321
+ parameters: BiomeCheckParams,
322
+ success: BiomeCheckResult,
323
+ failure: McpToolError
324
+ }).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(BiomeCheckAsMarkdown));
325
+ const isMcpToolError = (u) => u instanceof InvalidArgument || u instanceof BiomeUnavailable || u instanceof BiomeFailed;
326
+ /**
327
+ * Wire handler: {@link runBiomeCheck} lifted into Effect. The typed members it
328
+ * throws pass through unchanged; anything else (a defect in the parser, say)
329
+ * is reported as {@link BiomeFailed} so the error channel stays closed over
330
+ * {@link McpToolError}.
331
+ */
332
+ const handleBiomeCheck = (fallbackCwd, params) => Effect.tryPromise({
333
+ try: () => runBiomeCheck(params, fallbackCwd),
334
+ catch: (cause) => isMcpToolError(cause) ? cause : biomeFailed(`Biome check failed: ${cause instanceof Error ? cause.message : String(cause)}`)
335
+ });
283
336
 
284
337
  //#endregion
285
- export { BiomeCheckAsMarkdown, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, buildBiomeResult, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
338
+ export { BiomeCheckAsMarkdown, BiomeCheckParams, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, biomeCheckTool, buildBiomeResult, handleBiomeCheck, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { Changesets } from "@savvy-web/silk-effects";
3
- import { Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
 
5
8
  //#region src/tools/changeset-deps-detect.ts
6
9
  /** One affected workspace package's resolved dependency diff. */
@@ -100,6 +103,28 @@ const changesetDepsDetect = (args, fallbackCwd) => Effect.gen(function* () {
100
103
  }))
101
104
  };
102
105
  });
106
+ /** Wire parameters for `changeset_deps_detect`. */
107
+ const ChangesetDepsDetectParams = Schema.Struct({
108
+ base: Schema.optionalKey(Schema.String.annotate({ description: "Override the base branch used to compute the merge-base." })),
109
+ package: Schema.optionalKey(Schema.String.annotate({ description: "Restrict output to a single workspace package." })),
110
+ packages: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Restrict output to these workspace packages (unioned with package)." })),
111
+ exclude: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Drop these packages from the output entirely." })),
112
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
113
+ });
114
+ const REMEDIATION = {
115
+ hint: "The dependency diff could not be planned; check that the base branch exists locally and that every named package is a workspace member.",
116
+ suggestedTool: "workspace_info"
117
+ };
118
+ /** The `changeset_deps_detect` tool value. */
119
+ const changesetDepsDetectTool = Tool.make("changeset_deps_detect", {
120
+ description: "Read-only preview of the cumulative dependency diff (merge-base -> working tree) per workspace package. Returns each affected package's resolved dependency-table rows (catalog:/workspace: specifiers resolved per side; devDependencies retained) as the exact rows a pure-dependency changeset would carry, plus a coexisting list of untouched prose-only changesets that reference an in-scope package (informational — no need to re-list .changeset/). Does NOT write or delete any file. Prefer this over shelling out to savvy changeset deps detect.",
121
+ parameters: ChangesetDepsDetectParams,
122
+ success: ChangesetDepsDetectResult,
123
+ failure: McpToolError,
124
+ dependencies: [WorkspaceRoot, Changesets.DepsRegen]
125
+ }).annotate(Tool.Title, "Detect dependency changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetDepsDetectAsMarkdown));
126
+ /** Wire handler: {@link changesetDepsDetect} with its error channel mapped onto {@link McpToolError}. */
127
+ const handleChangesetDepsDetect = (fallbackCwd, params) => changesetDepsDetect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
103
128
 
104
129
  //#endregion
105
- export { ChangesetDepsDetectAsMarkdown, ChangesetDepsDetectCoexisting, ChangesetDepsDetectPackage, ChangesetDepsDetectResult, changesetDepsDetect };
130
+ export { ChangesetDepsDetectAsMarkdown, ChangesetDepsDetectCoexisting, ChangesetDepsDetectPackage, ChangesetDepsDetectParams, ChangesetDepsDetectResult, changesetDepsDetect, changesetDepsDetectTool, handleChangesetDepsDetect };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { Changesets } from "@savvy-web/silk-effects";
3
- import { Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
 
5
8
  //#region src/tools/changeset-deps-regen.ts
6
9
  /**
@@ -110,6 +113,29 @@ const changesetDepsRegen = (args, fallbackCwd) => Effect.gen(function* () {
110
113
  dryRun: false
111
114
  };
112
115
  });
116
+ /** Wire parameters for `changeset_deps_regen`. */
117
+ const ChangesetDepsRegenParams = Schema.Struct({
118
+ base: Schema.optionalKey(Schema.String.annotate({ description: "Override the base branch used to compute the merge-base." })),
119
+ package: Schema.optionalKey(Schema.String.annotate({ description: "Restrict regeneration to a single workspace package." })),
120
+ packages: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Restrict regeneration to these workspace packages (unioned with package)." })),
121
+ exclude: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Skip these packages entirely: nothing written, existing changesets untouched." })),
122
+ dryRun: Schema.optionalKey(Schema.Boolean.annotate({ description: "Compute the plan without writing or deleting any file." })),
123
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
124
+ });
125
+ const REMEDIATION = {
126
+ hint: "The dependency changesets could not be regenerated; preview the plan with dryRun=true, and check that the base branch exists locally and that every named package is a workspace member.",
127
+ suggestedTool: "changeset_deps_detect"
128
+ };
129
+ /** The `changeset_deps_regen` tool value. Mutating: not read-only, not idempotent. */
130
+ const changesetDepsRegenTool = Tool.make("changeset_deps_regen", {
131
+ description: "Regenerate pure-dependency changesets: delete stale single-package Dependencies-only changesets and write fresh single-package, patch-bump changesets from the cumulative dependency diff (catalog:/workspace: resolved; devDependencies dropped). Mixed changesets (Dependencies plus other content) are left untouched, and the result's coexisting list accounts for untouched prose-only changesets that reference an in-scope package (informational — no need to re-list .changeset/). Set dryRun=true to preview the plan without touching the filesystem. NOTE: without dryRun this tool MUTATES .changeset/*.md (git-reversible). Prefer this over shelling out to savvy changeset deps regen.",
132
+ parameters: ChangesetDepsRegenParams,
133
+ success: ChangesetDepsRegenResult,
134
+ failure: McpToolError,
135
+ dependencies: [WorkspaceRoot, Changesets.DepsRegen]
136
+ }).annotate(Tool.Title, "Regenerate dependency changesets").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetDepsRegenAsMarkdown));
137
+ /** Wire handler: {@link changesetDepsRegen} with its error channel mapped onto {@link McpToolError}. */
138
+ const handleChangesetDepsRegen = (fallbackCwd, params) => changesetDepsRegen(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
113
139
 
114
140
  //#endregion
115
- export { ChangesetCoexistingEntry, ChangesetDepsRegenAsMarkdown, ChangesetDepsRegenResult, changesetDepsRegen };
141
+ export { ChangesetCoexistingEntry, ChangesetDepsRegenAsMarkdown, ChangesetDepsRegenParams, ChangesetDepsRegenResult, changesetDepsRegen, changesetDepsRegenTool, handleChangesetDepsRegen };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { Changesets } from "@savvy-web/silk-effects";
3
- import { Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
 
5
8
  //#region src/tools/changeset-inspect.ts
6
9
  /** Branch-analysis variant. */
@@ -124,6 +127,35 @@ const changesetInspect = (args, fallbackCwd) => Effect.gen(function* () {
124
127
  };
125
128
  }
126
129
  });
130
+ /** Wire parameters for `changeset_inspect`. */
131
+ const ChangesetInspectParams = Schema.Struct({
132
+ mode: Schema.Literals([
133
+ "branch",
134
+ "config",
135
+ "classify"
136
+ ]).annotate({ description: "Which inspection to run." }),
137
+ base: Schema.optionalKey(Schema.String.annotate({ description: "Override the base branch (branch mode only)." })),
138
+ paths: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Paths to classify (classify mode only)." })),
139
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
140
+ });
141
+ const REMEDIATION = {
142
+ hint: "Check .changeset/config.json and that the base branch exists locally (fetch it if the merge base cannot be computed).",
143
+ suggestedTool: "changeset_inspect"
144
+ };
145
+ /** The `changeset_inspect` tool value. */
146
+ const changesetInspectTool = Tool.make("changeset_inspect", {
147
+ description: "Read-only changeset analysis for the changeset-manager workflow. mode=branch diffs the current branch against its base and classifies every changed file by owning package (with packagesAffected and the unmapped paths to ask the user about; an unmapped path may carry a machine-readable unmappedHint reason — e.g. a deleted versionFiles/additionalScopes target or a known template mirror — meaning it is probably already accounted for). mode=config surfaces the resolved .changeset/config.json (release surfaces, versionFiles, ignore list). mode=classify maps arbitrary repo-relative paths to their owning package. Prefer this over shelling out to the savvy CLI.",
148
+ parameters: ChangesetInspectParams,
149
+ success: ChangesetInspectResult,
150
+ failure: McpToolError,
151
+ dependencies: [
152
+ Changesets.BranchAnalyzer,
153
+ Changesets.ConfigInspector,
154
+ WorkspaceRoot
155
+ ]
156
+ }).annotate(Tool.Title, "Inspect changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetInspectAsMarkdown));
157
+ /** Wire handler: {@link changesetInspect} with its error channel mapped onto {@link McpToolError}. */
158
+ const handleChangesetInspect = (fallbackCwd, params) => changesetInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
127
159
 
128
160
  //#endregion
129
- export { ChangesetBranchResult, ChangesetClassifyResult, ChangesetConfigResult, ChangesetInspectAsMarkdown, ChangesetInspectResult, changesetInspect };
161
+ export { ChangesetBranchResult, ChangesetClassifyResult, ChangesetConfigResult, ChangesetInspectAsMarkdown, ChangesetInspectParams, ChangesetInspectResult, changesetInspect, changesetInspectTool, handleChangesetInspect };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { Changesets } from "@savvy-web/silk-effects";
3
- import { Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
 
5
8
  //#region src/tools/changeset-preview.ts
6
9
  /** The `changeset_preview` result — the silk-effects preview shape. */
@@ -39,6 +42,22 @@ const changesetPreview = (args, fallbackCwd) => Effect.gen(function* () {
39
42
  const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
40
43
  return yield* (yield* Changesets.ReleasePlanner).preview(root);
41
44
  });
45
+ /** Wire parameters for `changeset_preview`. */
46
+ const ChangesetPreviewParams = Schema.Struct({ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." })) });
47
+ const REMEDIATION = {
48
+ hint: "The changesets engine could not render the release; validate the pending changesets first.",
49
+ suggestedTool: "changeset_validate"
50
+ };
51
+ /** The `changeset_preview` tool value. */
52
+ const changesetPreviewTool = Tool.make("changeset_preview", {
53
+ description: "Read-only preview of the next release. Runs the genuine changesets engine over the pending changesets and returns each package's version bump (old -> new) plus the rendered CHANGELOG block (dependency tables included), exactly as it would ship. Does not modify the repo. Prefer this over hand-merging changeset files.",
54
+ parameters: ChangesetPreviewParams,
55
+ success: ChangesetPreviewResult,
56
+ failure: McpToolError,
57
+ dependencies: [Changesets.ReleasePlanner, WorkspaceRoot]
58
+ }).annotate(Tool.Title, "Preview the next release").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetPreviewAsMarkdown));
59
+ /** Wire handler: {@link changesetPreview} with its error channel mapped onto {@link McpToolError}. */
60
+ const handleChangesetPreview = (fallbackCwd, params) => changesetPreview(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
42
61
 
43
62
  //#endregion
44
- export { ChangesetPreviewAsMarkdown, ChangesetPreviewResult, changesetPreview };
63
+ export { ChangesetPreviewAsMarkdown, ChangesetPreviewParams, ChangesetPreviewResult, changesetPreview, changesetPreviewTool, handleChangesetPreview };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, invalidArgument, mapEngineError, truncateEchoed } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Data, Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { Changesets } from "@savvy-web/silk-effects";
3
- import { Data, Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
  import { resolve } from "node:path";
5
8
 
6
9
  //#region src/tools/changeset-validate.ts
@@ -79,6 +82,29 @@ const changesetValidate = (args, fallbackCwd) => Effect.gen(function* () {
79
82
  messages
80
83
  };
81
84
  });
85
+ /** Wire parameters for `changeset_validate`. */
86
+ const ChangesetValidateParams = Schema.Struct({
87
+ dir: Schema.optionalKey(Schema.String.annotate({ description: "Changeset directory to validate (default .changeset)." })),
88
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
89
+ });
90
+ const DIR_REMEDIATION = { hint: "Pass dir as a path (relative to the workspace root) to an existing changeset directory, or omit it for .changeset." };
91
+ /** The `changeset_validate` tool value. */
92
+ const changesetValidateTool = Tool.make("changeset_validate", {
93
+ description: "Read-only validation of changeset files against the section-aware rules. Pass dir (default .changeset). Returns typed diagnostics (file, rule, line, column, message) plus ok/errorCount in structuredContent. Prefer this over shelling out to savvy changeset lint.",
94
+ parameters: ChangesetValidateParams,
95
+ success: ChangesetValidateResult,
96
+ failure: McpToolError,
97
+ dependencies: [WorkspaceRoot]
98
+ }).annotate(Tool.Title, "Validate changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ChangesetValidateAsMarkdown));
99
+ /**
100
+ * Wire handler: {@link changesetValidate} with its error channel mapped onto
101
+ * {@link McpToolError}. The typed {@link ChangesetValidateError} (a thrown
102
+ * validate — in practice a missing directory) is an argument problem, so it
103
+ * becomes {@link InvalidArgument} naming `dir`; the echoed directory is
104
+ * truncated.
105
+ */
106
+ const handleChangesetValidate = (fallbackCwd, params) => changesetValidate(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "ChangesetValidateError" ? invalidArgument("dir", `Changeset directory "${truncateEchoed(error.dir)}" could not be validated: ${describeCause(error.cause)}`, DIR_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, DIR_REMEDIATION)(error)));
107
+ const describeCause = (cause) => cause instanceof Error ? truncateEchoed(cause.message) : truncateEchoed(String(cause));
82
108
 
83
109
  //#endregion
84
- export { ChangesetLintMessage, ChangesetValidateAsMarkdown, ChangesetValidateError, ChangesetValidateResult, changesetValidate };
110
+ export { ChangesetLintMessage, ChangesetValidateAsMarkdown, ChangesetValidateError, ChangesetValidateParams, ChangesetValidateResult, changesetValidate, changesetValidateTool, handleChangesetValidate };
@@ -1,7 +1,10 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
1
3
  import { mdInline } from "./md-inline.js";
4
+ import { Effect, FileSystem, Option, Path, Result, Schema, SchemaGetter } from "effect";
2
5
  import { WorkspaceRoot } from "@effected/workspaces";
3
6
  import { Repos } from "@savvy-web/silk-effects";
4
- import { Effect, FileSystem, Option, Path, Result, Schema, SchemaGetter } from "effect";
7
+ import { Tool } from "effect/unstable/ai";
5
8
  import { Gitmodules } from "@effected/git";
6
9
 
7
10
  //#region src/tools/repos-inspect.ts
@@ -186,6 +189,39 @@ const reposInspect = (args, fallbackCwd) => Effect.gen(function* () {
186
189
  }
187
190
  }
188
191
  });
192
+ /**
193
+ * The `repos_inspect` wire-level `mode` enum. Exported so tests can assert
194
+ * the boundary rejects an unknown mode without duplicating the member list.
195
+ */
196
+ const ReposInspectMode = Schema.Literals([
197
+ "status",
198
+ "config",
199
+ "drift",
200
+ "gitmodules"
201
+ ]).annotate({ description: "status = drift report; config = the full agent brief; drift = five-authority submodule reconciliation; gitmodules = decoded .gitmodules sections." });
202
+ /** Wire parameters for `repos_inspect`. */
203
+ const ReposInspectParams = Schema.Struct({
204
+ mode: ReposInspectMode,
205
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
206
+ });
207
+ const REMEDIATION = { hint: "The vendored-repo state could not be read; check .repos/config.json and .gitmodules, and that git can run in the workspace." };
208
+ /** The `repos_inspect` tool value. */
209
+ const reposInspectTool = Tool.make("repos_inspect", {
210
+ description: "Read-only: drift report or parsed .repos/config.json manifest with orientation and notes. mode=status is the per-repo drift summary from ReposManager (present/dirty/commit); mode=config is the parsed manifest; mode=drift reconciles all four submodule authorities (manifest, .gitmodules, worktree, git submodule status) and reports every disagreement; mode=gitmodules decodes the raw .gitmodules file's submodule sections.",
211
+ parameters: ReposInspectParams,
212
+ success: ReposInspectResult,
213
+ failure: McpToolError,
214
+ dependencies: [
215
+ Repos.ReposManager,
216
+ Repos.ReposConfigStore,
217
+ Repos.ReposDrift,
218
+ WorkspaceRoot,
219
+ FileSystem.FileSystem,
220
+ Path.Path
221
+ ]
222
+ }).annotate(Tool.Title, "Inspect vendored repos").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ReposInspectAsMarkdown));
223
+ /** Wire handler: {@link reposInspect} with its error channel mapped onto {@link McpToolError}. */
224
+ const handleReposInspect = (fallbackCwd, params) => reposInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
189
225
 
190
226
  //#endregion
191
- export { ReposConfigResult, ReposDriftResult, ReposGitmodulesResult, ReposInspectAsMarkdown, ReposInspectResult, ReposStatusResult, reposInspect };
227
+ export { ReposConfigResult, ReposDriftResult, ReposGitmodulesResult, ReposInspectAsMarkdown, ReposInspectMode, ReposInspectParams, ReposInspectResult, ReposStatusResult, handleReposInspect, reposInspect, reposInspectTool };
@@ -1,7 +1,10 @@
1
+ import { ENGINE_ECHO_LIMIT, McpToolError, invalidArgument, mapEngineError, truncateEchoed } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
1
3
  import { mdInline } from "./md-inline.js";
4
+ import { Effect, Schema, SchemaGetter } from "effect";
2
5
  import { WorkspaceRoot } from "@effected/workspaces";
3
6
  import { Repos } from "@savvy-web/silk-effects";
4
- import { Effect, Schema, SchemaGetter } from "effect";
7
+ import { Tool } from "effect/unstable/ai";
5
8
 
6
9
  //#region src/tools/repos-manage.ts
7
10
  /** `sync` has no extra fields. */
@@ -327,6 +330,59 @@ const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
327
330
  };
328
331
  }
329
332
  });
333
+ /** Wire parameters for `repos_manage`: flat (no `oneOf`); the handler decodes them per action. */
334
+ const ReposManageParams = Schema.Struct({
335
+ action: Schema.Literals([
336
+ "sync",
337
+ "pin",
338
+ "add",
339
+ "note",
340
+ "remove",
341
+ "rename",
342
+ "restore",
343
+ "deregister"
344
+ ]).annotate({ description: "Which mutation to perform." }),
345
+ name: Schema.optionalKey(Schema.String.annotate({ description: "Repo name (pin, note, remove, rename; optional override for add)." })),
346
+ newName: Schema.optionalKey(Schema.String.annotate({ description: "New repo name (rename)." })),
347
+ ref: Schema.optionalKey(Schema.String.annotate({ description: "Git ref to pin/vendor to (pin, add)." })),
348
+ url: Schema.optionalKey(Schema.String.annotate({ description: "Repo URL to vendor (add)." })),
349
+ purpose: Schema.optionalKey(Schema.String.annotate({ description: "One-line purpose for the manifest (add)." })),
350
+ sparse: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Sparse-checkout patterns (add)." })),
351
+ orientation: Schema.optionalKey(Repos.RepoOrientation.annotate({ description: "Orientation block to write (add). Pass back what a preceding remove reported as removedEntry.orientation — add does NOT restore it on its own, so a re-vendor loses it otherwise." })),
352
+ op: Schema.optionalKey(Schema.Literals([
353
+ "add",
354
+ "remove",
355
+ "promote"
356
+ ]).annotate({ description: "Note operation (note)." })),
357
+ note: Schema.optionalKey(Schema.String.annotate({ description: "Note text (note, op=add)." })),
358
+ id: Schema.optionalKey(Schema.String.annotate({ description: "Note id (note, op=remove|promote)." })),
359
+ into: Schema.optionalKey(Schema.Literals(["layout", "startHere"]).annotate({ description: "Orientation target (note, op=promote)." })),
360
+ names: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Repo names to restore (restore); omitted restores every dirty repo." })),
361
+ section: Schema.optionalKey(Schema.String.annotate({ description: "Stale registration name to clear (deregister), exactly as the drift report states it (e.g. .repos/old-name); the submodule. prefix is implied." })),
362
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
363
+ });
364
+ const REMEDIATION = {
365
+ hint: "The mutation did not complete; inspect the vendored-repo state before retrying.",
366
+ suggestedTool: "repos_inspect"
367
+ };
368
+ const REQUEST_REMEDIATION = { hint: "Pass the fields the chosen action needs (pin: name+ref; add: url+ref+purpose; note: name+op plus note/id/into; remove: name; rename: name+newName; deregister: section)." };
369
+ /** The `repos_manage` tool value. Mutating: not read-only, not idempotent. */
370
+ const reposManageTool = Tool.make("repos_manage", {
371
+ description: "Mutating: sync (initialize/reconcile submodules per the manifest), pin (re-pin a repo to a new ref), add (vendor a new repo), note (add/remove/promote an agent note), remove (unvendor a repo), rename (rename a vendored repo's manifest key and worktree), restore (hard-reset a repo's worktree back to its pinned gitlink commit and re-apply sparse paths — DESTRUCTIVE to uncommitted worktree edits; never run implicitly), or deregister (clear a STALE submodule.<section> registration from the superproject's local git config — the phantom entry repos_inspect drift reports as localRegistrationDivergence with no matching manifest entry; refuses a section outside .repos/ and any section still backing a live manifest entry — canonically named or gitdir-diverged — and touches local config only, so nothing is staged). Pass action plus the fields that action needs: pin needs name+ref; add needs url+ref+purpose (name/sparse/orientation optional — pass orientation back from a preceding remove's removedEntry to make a re-vendor lossless); note needs name+op, plus note (op=add), id (op=remove), or id+into (op=promote); remove needs name; rename needs name (the old name) + newName; restore takes an optional names list — omitted, it restores every dirty repo and reports the clean ones as skipped; given, it restores exactly those repos even if already clean; deregister needs section (the registration name exactly as the drift report states it, e.g. .repos/old-name — no submodule. prefix). A decode failure names the missing field. The pin result's markdown surfaces commitMessage and staleNoteIds — review and commit after pinning. The remove result's markdown surfaces commitMessage, removedNotes and the removed entry's orientation block — promote any durable notes elsewhere, keep the orientation if you intend to re-vendor, then review and commit. The rename result's markdown surfaces commitMessage — review and commit after renaming. The restore result's markdown names exactly what was discarded. The deregister result's markdown lists the config keys the removed section carried — nothing to commit afterwards.",
372
+ parameters: ReposManageParams,
373
+ success: ReposManageResult,
374
+ failure: McpToolError,
375
+ dependencies: [Repos.ReposManager, WorkspaceRoot]
376
+ }).annotate(Tool.Title, "Manage vendored repos").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(ReposManageAsMarkdown));
377
+ /**
378
+ * Wire handler: {@link reposManage} with its error channel mapped onto
379
+ * {@link McpToolError}. The per-action request decode failure (`SchemaError`,
380
+ * which names the missing field, and echoes the decoded value) is an argument
381
+ * problem and becomes {@link InvalidArgument} keyed on `action`, its text
382
+ * truncated at {@link ENGINE_ECHO_LIMIT}; the engine's own errors go
383
+ * through {@link mapEngineError}.
384
+ */
385
+ const handleReposManage = (fallbackCwd, params) => reposManage(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "SchemaError" ? invalidArgument("action", `repos_manage ${params.action}: ${truncateEchoed(error.message, ENGINE_ECHO_LIMIT)}`, REQUEST_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)(error)));
330
386
 
331
387
  //#endregion
332
- export { ReposManageAddResult, ReposManageAsMarkdown, ReposManageDeregisterResult, ReposManageNoteResult, ReposManagePinResult, ReposManageRemoveResult, ReposManageRenameResult, ReposManageRestoreResult, ReposManageResult, ReposManageSyncResult, reposManage };
388
+ export { ReposManageAddResult, ReposManageAsMarkdown, ReposManageDeregisterResult, ReposManageNoteResult, ReposManageParams, ReposManagePinResult, ReposManageRemoveResult, ReposManageRenameResult, ReposManageRestoreResult, ReposManageResult, ReposManageSyncResult, handleReposManage, reposManage, reposManageTool };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { Turbo } from "@savvy-web/silk-effects";
3
- import { Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
 
5
8
  //#region src/tools/turbo-inspect.ts
6
9
  /** Cache-diagnosis variant of the `turbo_inspect` result. */
@@ -104,6 +107,28 @@ const turboInspect = (args, fallbackCwd) => Effect.gen(function* () {
104
107
  };
105
108
  }
106
109
  });
110
+ /** Wire parameters for `turbo_inspect`. */
111
+ const TurboInspectParams = Schema.Struct({
112
+ mode: Schema.Literals([
113
+ "cache",
114
+ "graph",
115
+ "affected"
116
+ ]).annotate({ description: "Which inspection to run." }),
117
+ task: Schema.optionalKey(Schema.String.annotate({ description: "Task name (defaults to build:dev for cache/graph)." })),
118
+ base: Schema.optionalKey(Schema.String.annotate({ description: "Base git ref for affected mode." })),
119
+ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Directory to resolve the workspace root from." }))
120
+ });
121
+ const REMEDIATION = { hint: "turbo could not complete the dry run; check that the task exists in turbo.json and that turbo resolves from the workspace." };
122
+ /** The `turbo_inspect` tool value. */
123
+ const turboInspectTool = Tool.make("turbo_inspect", {
124
+ description: "Read-only Turborepo inspection. mode=cache diagnoses why a task's cache is hitting/missing (per-package status plus the exact hash contributors: input files, env vars, external-dep hashes, global hash). mode=graph returns the task graph and critical path. mode=affected lists changed packages and their dependents. Never executes tasks (uses --dry).",
125
+ parameters: TurboInspectParams,
126
+ success: TurboInspectResult,
127
+ failure: McpToolError,
128
+ dependencies: [Turbo.TurboInspector, WorkspaceRoot]
129
+ }).annotate(Tool.Title, "Inspect Turborepo").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(TurboInspectAsMarkdown));
130
+ /** Wire handler: {@link turboInspect} with its error channel mapped onto {@link McpToolError}. */
131
+ const handleTurboInspect = (fallbackCwd, params) => turboInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
107
132
 
108
133
  //#endregion
109
- export { TurboAffectedResult, TurboCacheResult, TurboGraphResult, TurboInspectAsMarkdown, TurboInspectResult, turboInspect };
134
+ export { TurboAffectedResult, TurboCacheResult, TurboGraphResult, TurboInspectAsMarkdown, TurboInspectParams, TurboInspectResult, handleTurboInspect, turboInspect, turboInspectTool };
@@ -1,6 +1,9 @@
1
+ import { McpToolError, mapEngineError } from "../errors.js";
2
+ import { SilkMarkdown } from "../markdown.js";
3
+ import { Effect, Schema, SchemaGetter } from "effect";
1
4
  import { WorkspaceRoot } from "@effected/workspaces";
2
5
  import { SilkWorkspaceAnalyzer } from "@savvy-web/silk-effects";
3
- import { Effect, Schema, SchemaGetter } from "effect";
6
+ import { Tool } from "effect/unstable/ai";
4
7
 
5
8
  //#region src/tools/workspace-info.ts
6
9
  /** A flattened, non-recursive summary of one analyzed workspace. */
@@ -93,6 +96,26 @@ const workspaceInfo = (base) => Effect.gen(function* () {
93
96
  const analysis = yield* (yield* SilkWorkspaceAnalyzer).analyze(root);
94
97
  return toWorkspaceInfoResult(analysis);
95
98
  });
99
+ /** Wire parameters for `workspace_info`. */
100
+ const WorkspaceInfoParams = Schema.Struct({ cwd: Schema.optionalKey(Schema.String.annotate({ description: "Workspace root to analyze. Defaults to the server's project dir." })) });
101
+ const REMEDIATION = { hint: "The workspace could not be analyzed; check the manifest and lockfile at the root named in the message." };
102
+ /**
103
+ * The `workspace_info` tool value. `dependencies` names the two services the
104
+ * handler yields so `Tool.HandlerServices` carries them (without it the
105
+ * handler record fails against `Toolkit.HandlersFrom`).
106
+ */
107
+ const workspaceInfoTool = Tool.make("workspace_info", {
108
+ description: "Use when you need the Silk workspace layout: runtime, package manager, and a per-workspace summary (publishability, versioning, tag/release state). Prefer this over running shell commands to inspect the workspace. Returns markdown in content[] and a typed object in structuredContent.",
109
+ parameters: WorkspaceInfoParams,
110
+ success: WorkspaceInfoResult,
111
+ failure: McpToolError,
112
+ dependencies: [SilkWorkspaceAnalyzer, WorkspaceRoot]
113
+ }).annotate(Tool.Title, "Workspace info").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false).annotate(SilkMarkdown, Schema.decodeUnknownSync(WorkspaceInfoAsMarkdown));
114
+ /** Wire handler: the existing {@link workspaceInfo} program with its error channel mapped onto {@link McpToolError}. */
115
+ const handleWorkspaceInfo = (fallbackCwd, params) => {
116
+ const cwd = params.cwd ?? fallbackCwd;
117
+ return workspaceInfo(cwd).pipe(Effect.mapError(mapEngineError(cwd, REMEDIATION)));
118
+ };
96
119
 
97
120
  //#endregion
98
- export { WorkspaceInfoAsMarkdown, WorkspaceInfoResult, WorkspaceSummary, formatWorkspaceInfoMarkdown, toWorkspaceInfoResult, workspaceInfo };
121
+ export { WorkspaceInfoAsMarkdown, WorkspaceInfoParams, WorkspaceInfoResult, WorkspaceSummary, formatWorkspaceInfoMarkdown, handleWorkspaceInfo, toWorkspaceInfoResult, workspaceInfo, workspaceInfoTool };
package/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- const CURRENT_MCP_VERSION = "0.0.0";
9
+ const CURRENT_MCP_VERSION = "3.0.0";
10
10
 
11
11
  //#endregion
12
12
  export { CURRENT_MCP_VERSION };