@savvy-web/mcp 3.2.3 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors.js +23 -81
- package/index.d.ts +39 -59
- package/index.js +2 -3
- package/main.d.ts +17 -12
- package/main.js +16 -21
- package/package.json +6 -3
- package/server.js +20 -176
- package/tools/biome-check.js +10 -29
- package/tools/changeset-deps-detect.js +4 -48
- package/tools/changeset-deps-regen.js +4 -50
- package/tools/changeset-inspect.js +4 -78
- package/tools/changeset-preview.js +4 -27
- package/tools/changeset-validate.js +8 -31
- package/tools/repos-inspect.js +5 -83
- package/tools/repos-manage.js +18 -142
- package/tools/turbo-inspect.js +4 -58
- package/tools/workspace-info.js +4 -26
- package/version.js +1 -1
- package/internal/project-root.js +0 -25
- package/markdown.js +0 -33
- package/tools/md-inline.js +0 -22
package/server.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { SilkMarkdown } from "./markdown.js";
|
|
2
1
|
import { makeSilkRuntimeLayer } from "./runtime.js";
|
|
3
2
|
import { SilkToolkit, ToolsLayer } from "./toolkit.js";
|
|
4
3
|
import { CURRENT_MCP_VERSION } from "./version.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { HttpServerRequest } from "effect/unstable/http";
|
|
4
|
+
import { McpStdio, McpToolkit } from "@effected/mcp";
|
|
5
|
+
import { Layer, Option } from "effect";
|
|
6
|
+
import { distributionSuffix } from "@effected/engine";
|
|
9
7
|
|
|
10
8
|
//#region src/server.ts
|
|
11
9
|
/**
|
|
@@ -25,187 +23,33 @@ const SERVER_INSTRUCTIONS = [
|
|
|
25
23
|
"changeset_inspect, changeset_validate, changeset_preview: the branch diff by owning package, typed CSH001-CSH005 diagnostics, and the CHANGELOG the pending changesets would produce.",
|
|
26
24
|
"changeset_deps_detect (reads) and changeset_deps_regen (deletes and recreates) dependency changesets.",
|
|
27
25
|
"repos_inspect (reads) and repos_manage (mutates) the vendored reference repos under .repos/; repos_manage restore discards uncommitted worktree edits.",
|
|
28
|
-
"Every successful result
|
|
26
|
+
"Every successful result is a typed object in structuredContent (content[0].text carries the same object as JSON). A failure is an isError result whose content[0].text carries the message and remediation, with no structuredContent. Every tool takes an optional cwd; omit it to use the server's project directory."
|
|
29
27
|
].join("\n");
|
|
30
|
-
const INTERNAL_TOOL_ERROR_MESSAGE = "Tool execution failed due to an internal server error.";
|
|
31
|
-
const toolErrorResult = (message) => new McpSchema.CallToolResult({
|
|
32
|
-
isError: true,
|
|
33
|
-
content: [{
|
|
34
|
-
type: "text",
|
|
35
|
-
text: message
|
|
36
|
-
}]
|
|
37
|
-
});
|
|
38
|
-
const toolResultContent = (encoded) => encoded === void 0 ? [] : [{
|
|
39
|
-
type: "text",
|
|
40
|
-
text: JSON.stringify(encoded)
|
|
41
|
-
}];
|
|
42
|
-
const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, CurrentLogLevel);
|
|
43
|
-
const isParameterValidationError = (error) => AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError";
|
|
44
28
|
/**
|
|
45
|
-
*
|
|
46
|
-
* `
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* so without this no tool would serve one. The definitions stay attached
|
|
50
|
-
* for nested refs. (rc.116's own `registerToolkit` inlines the top-level
|
|
51
|
-
* reference of the INPUT schema through an internal helper and serves the
|
|
52
|
-
* output document verbatim; this is the public-API equivalent for both.)
|
|
29
|
+
* The toolkit registration as a layer: `McpToolkit.layer` over core's
|
|
30
|
+
* module-level `McpServer.McpServer.layer` — the same reference
|
|
31
|
+
* `McpStdio.layer` serves, so layer memoization lands the registration on the
|
|
32
|
+
* served instance — with the handlers bound to `cwd`.
|
|
53
33
|
*/
|
|
54
|
-
const
|
|
55
|
-
const ref = schema.$ref;
|
|
56
|
-
const defs = schema.$defs;
|
|
57
|
-
if (typeof ref !== "string" || !ref.startsWith("#/$defs/") || typeof defs !== "object" || defs === null) return schema;
|
|
58
|
-
const target = defs[ref.slice(8)];
|
|
59
|
-
if (typeof target !== "object" || target === null) return schema;
|
|
60
|
-
const { $ref: _ref, ...rest } = schema;
|
|
61
|
-
return {
|
|
62
|
-
...rest,
|
|
63
|
-
...target,
|
|
64
|
-
$defs: defs
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* The served input schema: MCP requires an object root, so a top-level `$ref`
|
|
69
|
-
* is inlined, and a strict tool is emitted with `onExcessProperty: "error"`
|
|
70
|
-
* so the client sees `additionalProperties: false`. Mirrors rc.116's
|
|
71
|
-
* `toolInputJsonSchema` through public API (`Schema.toJsonSchemaDocument` +
|
|
72
|
-
* {@link hoistRootRef} in place of the internal `resolveTopLevelReference`).
|
|
73
|
-
*/
|
|
74
|
-
const toolInputJsonSchema = (schema, strict) => {
|
|
75
|
-
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
|
|
76
|
-
const withDefs = Object.keys(document.definitions).length === 0 ? document.schema : {
|
|
77
|
-
...document.schema,
|
|
78
|
-
$defs: document.definitions
|
|
79
|
-
};
|
|
80
|
-
return hoistRootRef(withDefs);
|
|
81
|
-
};
|
|
82
|
-
/** MCP models `structuredContent` as a JSON object, so a `null` or array encoded result is omitted. */
|
|
83
|
-
const toStructuredContent = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
84
|
-
/**
|
|
85
|
-
* Register a toolkit with the running `McpServer`, rendering each success as
|
|
86
|
-
* the tool's markdown transcript in `content[0].text` and the encoded result
|
|
87
|
-
* in `structuredContent`. A port of rc.116's `McpServer.registerToolkit`
|
|
88
|
-
* that differs in the success branch and the `outputSchema` gate only (see
|
|
89
|
-
* the module remarks).
|
|
90
|
-
*
|
|
91
|
-
* @remarks
|
|
92
|
-
* `Effect.context<never>()` captures the registration-time services (the
|
|
93
|
-
* handlers' declared dependencies, discharged by the runtime layer) so each
|
|
94
|
-
* call runs against them; the per-request services (`McpRequestContext`,
|
|
95
|
-
* the legacy `McpServerClient`, the HTTP request, the request log level) are
|
|
96
|
-
* omitted from that capture because the framework provides them per call.
|
|
97
|
-
*
|
|
98
|
-
* @internal exported for the in-process suite, which registers fixture
|
|
99
|
-
* toolkits (a strict tool, for one) through the same path the served
|
|
100
|
-
* toolkit takes.
|
|
101
|
-
*/
|
|
102
|
-
const registerSilkToolkit = (toolkit) => Effect.gen(function* () {
|
|
103
|
-
const registry = yield* McpServer.McpServer;
|
|
104
|
-
const built = yield* toolkit.pipe(Effect.updateContext((context) => {
|
|
105
|
-
const services = new Map(context.mapUnsafe);
|
|
106
|
-
for (const tool of Object.values(toolkit.tools)) {
|
|
107
|
-
const handler = services.get(tool.id);
|
|
108
|
-
if (handler !== void 0) services.set(tool.id, {
|
|
109
|
-
...handler,
|
|
110
|
-
context: omitRequestServices(handler.context)
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
return Context.makeUnsafe(services);
|
|
114
|
-
}));
|
|
115
|
-
const services = omitRequestServices(yield* Effect.context());
|
|
116
|
-
const reportCause = (cause) => Effect.provideContext(ErrorReporter.report(cause), services);
|
|
117
|
-
const internalToolError = (cause) => {
|
|
118
|
-
const failure = Cause.findFail(cause);
|
|
119
|
-
return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(cause).pipe(Effect.andThen(reportCause(cause)), Effect.as(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE)));
|
|
120
|
-
};
|
|
121
|
-
const registrations = [];
|
|
122
|
-
for (const tool of Object.values(built.tools)) {
|
|
123
|
-
const strict = Tool.getStrictMode(tool) === true;
|
|
124
|
-
const rawJsonSchema = Tool.isDynamic(tool) ? tool.jsonSchema : void 0;
|
|
125
|
-
if (strict && rawJsonSchema !== void 0) return yield* Effect.die(`McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`);
|
|
126
|
-
const decodeOptions = strict ? { onExcessProperty: "error" } : void 0;
|
|
127
|
-
const annotations = tool.annotations;
|
|
128
|
-
const renderMarkdown = Context.getOrUndefined(annotations, SilkMarkdown);
|
|
129
|
-
const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
|
|
130
|
-
const isDeclaredFailure = Schema.is(tool.failureSchema);
|
|
131
|
-
const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema);
|
|
132
|
-
const declaredFailureResult = (error) => error instanceof Error ? Effect.succeed(toolErrorResult(error.message)) : Effect.map(encodeFailure(error), (encoded) => new McpSchema.CallToolResult({
|
|
133
|
-
isError: true,
|
|
134
|
-
content: toolResultContent(encoded)
|
|
135
|
-
}));
|
|
136
|
-
const handleCause = (cause) => {
|
|
137
|
-
const failure = Cause.findFail(cause);
|
|
138
|
-
if (Result.isSuccess(failure)) {
|
|
139
|
-
const error = failure.success.error;
|
|
140
|
-
const origin = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin);
|
|
141
|
-
if (origin === "parameters" && isParameterValidationError(error)) return Effect.fail(new McpSchema.InvalidParams({ message: error.reason.message }));
|
|
142
|
-
if (origin === "handler" && isDeclaredFailure(error)) return Effect.logError(cause).pipe(Effect.andThen(Effect.catchCause(declaredFailureResult(error), internalToolError)));
|
|
143
|
-
}
|
|
144
|
-
return internalToolError(cause);
|
|
145
|
-
};
|
|
146
|
-
const outputJsonSchema = hoistRootRef(Tool.getJsonSchemaFromSchema(tool.successSchema));
|
|
147
|
-
const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(outputJsonSchema).pipe(Effect.orDie) : void 0;
|
|
148
|
-
const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(rawJsonSchema ?? toolInputJsonSchema(tool.parametersSchema, strict)).pipe(Effect.orDie);
|
|
149
|
-
const description = Tool.getDescription(tool);
|
|
150
|
-
const mcpTool = new McpSchema.Tool({
|
|
151
|
-
name: tool.name,
|
|
152
|
-
...description === void 0 ? {} : { description },
|
|
153
|
-
inputSchema,
|
|
154
|
-
...outputSchema === void 0 ? {} : { outputSchema },
|
|
155
|
-
annotations: {
|
|
156
|
-
...Context.getOption(annotations, Tool.Title).pipe(Option.map((title) => ({ title })), Option.getOrUndefined),
|
|
157
|
-
readOnlyHint: Context.get(annotations, Tool.Readonly),
|
|
158
|
-
destructiveHint: Context.get(annotations, Tool.Destructive),
|
|
159
|
-
idempotentHint: Context.get(annotations, Tool.Idempotent),
|
|
160
|
-
openWorldHint: Context.get(annotations, Tool.OpenWorld)
|
|
161
|
-
},
|
|
162
|
-
...toolMeta === void 0 ? {} : { _meta: toolMeta }
|
|
163
|
-
});
|
|
164
|
-
registrations.push({
|
|
165
|
-
tool: mcpTool,
|
|
166
|
-
annotations,
|
|
167
|
-
handle: (payload) => built.handle(tool.name, payload ?? {}, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => result.isFailure && result.failureOrigin !== "handler" ? Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result"))) : Effect.succeed(new McpSchema.CallToolResult({
|
|
168
|
-
isError: result.isFailure,
|
|
169
|
-
structuredContent: result.isFailure ? void 0 : toStructuredContent(result.encodedResult),
|
|
170
|
-
content: result.isFailure || renderMarkdown === void 0 ? toolResultContent(result.encodedResult) : [{
|
|
171
|
-
type: "text",
|
|
172
|
-
text: renderMarkdown(result.result)
|
|
173
|
-
}]
|
|
174
|
-
}))), Effect.catchCause(handleCause), Effect.provideContext(services))
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
for (const registration of registrations) yield* registry.addTool(registration);
|
|
178
|
-
});
|
|
179
|
-
/**
|
|
180
|
-
* The toolkit registration as a layer: {@link registerSilkToolkit} over
|
|
181
|
-
* `McpServer.layer` (the same reference `McpServer.layerStdio` merges, so
|
|
182
|
-
* layer memoization lands the registration on the served instance), with the
|
|
183
|
-
* handlers bound to `cwd`.
|
|
184
|
-
*/
|
|
185
|
-
const SilkToolsLayer = (cwd) => Layer.effectDiscard(registerSilkToolkit(SilkToolkit)).pipe(Layer.provide(McpServer.McpServer.layer), Layer.provide(ToolsLayer(cwd)));
|
|
34
|
+
const SilkToolsLayer = (cwd) => McpToolkit.layer(SilkToolkit, { strict: "annotated" }).pipe(Layer.provide(ToolsLayer(cwd)));
|
|
186
35
|
/**
|
|
187
36
|
* The whole server as one layer: the ten-tool toolkit, the silk-effects
|
|
188
|
-
* runtime discharging its dependencies, over `
|
|
37
|
+
* runtime discharging its dependencies, over `@effected/mcp`'s
|
|
38
|
+
* `McpStdio.layer`.
|
|
189
39
|
*
|
|
190
|
-
* `
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* failure there is an implementer-time defect, not a runtime condition.
|
|
40
|
+
* `McpStdio.layer` is `McpServer.layerStdio` with the kit's default
|
|
41
|
+
* `protocols` (the stateless `2026-07-28` adapter first, then the two newest
|
|
42
|
+
* stateful ones — see gotcha 2), `LogToStderr` merged into its output, a
|
|
43
|
+
* stdin guard answering a non-JSON line with `-32700`, and `Layer.orDie`.
|
|
195
44
|
*
|
|
196
45
|
* @public
|
|
197
46
|
*/
|
|
198
|
-
const ServerLayer = (cwd) => SilkToolsLayer(cwd).pipe(Layer.provide(makeSilkRuntimeLayer(cwd)), Layer.provide(
|
|
47
|
+
const ServerLayer = (cwd, options = {}) => SilkToolsLayer(cwd).pipe(Layer.provide(makeSilkRuntimeLayer(cwd)), Layer.provide(McpStdio.layer({
|
|
199
48
|
name: "savvy-mcp",
|
|
200
|
-
version: CURRENT_MCP_VERSION
|
|
49
|
+
version: `${CURRENT_MCP_VERSION}${distributionSuffix(Option.fromNullishOr(options.distribution))}`,
|
|
201
50
|
description: "Structured Silk Suite workspace tools: workspace, turbo, biome, changesets and vendored repos.",
|
|
202
|
-
instructions: SERVER_INSTRUCTIONS
|
|
203
|
-
|
|
204
|
-
McpProtocol.v2026_07_28,
|
|
205
|
-
McpProtocol.v2025_11_25,
|
|
206
|
-
McpProtocol.v2025_06_18
|
|
207
|
-
]
|
|
208
|
-
})), Layer.orDie);
|
|
51
|
+
instructions: SERVER_INSTRUCTIONS
|
|
52
|
+
})));
|
|
209
53
|
|
|
210
54
|
//#endregion
|
|
211
|
-
export { SERVER_INSTRUCTIONS, ServerLayer
|
|
55
|
+
export { SERVER_INSTRUCTIONS, ServerLayer };
|
package/tools/biome-check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BiomeFailed, BiomeUnavailable, InvalidArgument, McpToolError,
|
|
2
|
-
import {
|
|
3
|
-
import { Effect, Schema
|
|
1
|
+
import { BiomeFailed, BiomeUnavailable, InvalidArgument, McpToolError, invalidArgument } from "../errors.js";
|
|
2
|
+
import { ToolFailure } from "@effected/mcp";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
4
|
import { Lint } from "@savvy-web/silk-effects";
|
|
5
5
|
import { Tool } from "effect/unstable/ai";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -135,25 +135,6 @@ const buildBiomeResult = (params) => {
|
|
|
135
135
|
guidance: diagnostics.length === 0 ? GUIDANCE_ERRORS : guidance
|
|
136
136
|
};
|
|
137
137
|
};
|
|
138
|
-
/** Render the structured result as markdown. */
|
|
139
|
-
const renderMarkdown = (data) => {
|
|
140
|
-
if (data.diagnostics.length === 0) return `# biome — clean\n\n✅ No remaining diagnostics.${data.wrote ? " A --write pass ran; check `git diff` for what changed." : ""}`;
|
|
141
|
-
const upgraded = data.summary.upgradedWarnings ?? 0;
|
|
142
|
-
const upgradedNote = upgraded > 0 ? ` (${upgraded} strict-upgraded from project warnings)` : "";
|
|
143
|
-
const lines = [`# biome — ${data.summary.errors} error(s)${upgradedNote}, ${data.summary.warnings} warning(s)`, ``];
|
|
144
|
-
if (data.wrote) lines.push(`A --write pass ran; the diagnostics below remain unfixed.`, ``);
|
|
145
|
-
for (const d of data.diagnostics) {
|
|
146
|
-
const severity = d.originalSeverity ? `${d.severity} (project ${d.originalSeverity}, strict)` : d.severity;
|
|
147
|
-
lines.push(`- \`${d.file}:${d.line}\` **${severity}** ${d.rule} — ${d.message}`);
|
|
148
|
-
}
|
|
149
|
-
lines.push(``, `---`, data.guidance);
|
|
150
|
-
return lines.join("\n");
|
|
151
|
-
};
|
|
152
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
153
|
-
const BiomeCheckAsMarkdown = BiomeCheckResult.pipe(Schema.decodeTo(Schema.String, {
|
|
154
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
155
|
-
encode: SchemaGetter.forbidden(() => "BiomeCheckAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
156
|
-
}));
|
|
157
138
|
/**
|
|
158
139
|
* Canonicalize a path, falling back to lexical resolution when it does not exist
|
|
159
140
|
* (a non-existent target cannot be a symlink pointing out of tree).
|
|
@@ -220,7 +201,7 @@ const BIOME_FAILED_REMEDIATION = { hint: "Biome did not complete; the message ca
|
|
|
220
201
|
/** Build a {@link BiomeFailed} from a spawn error or a non-lint exit status. */
|
|
221
202
|
const biomeFailed = (raw, exitCode) => new BiomeFailed({
|
|
222
203
|
...exitCode === void 0 ? {} : { exitCode },
|
|
223
|
-
message:
|
|
204
|
+
message: ToolFailure.message(raw, BIOME_FAILED_REMEDIATION),
|
|
224
205
|
remediation: BIOME_FAILED_REMEDIATION
|
|
225
206
|
});
|
|
226
207
|
/**
|
|
@@ -240,17 +221,17 @@ const runBiomeCheck = async (args, fallbackCwd) => {
|
|
|
240
221
|
const root = canonicalize(fallbackCwd);
|
|
241
222
|
const cwd = canonicalize(args.cwd ?? fallbackCwd);
|
|
242
223
|
const containmentRoot = resolveContainmentRoot(root, cwd);
|
|
243
|
-
if (containmentRoot === null) throw invalidArgument("cwd", `cwd escapes the workspace root: ${
|
|
224
|
+
if (containmentRoot === null) throw invalidArgument("cwd", `cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
|
|
244
225
|
const within = (abs) => abs === containmentRoot || abs.startsWith(`${containmentRoot}${sep}`);
|
|
245
226
|
const paths = (args.paths && args.paths.length > 0 ? args.paths : ["."]).map((p) => {
|
|
246
227
|
const lexical = resolve(cwd, p);
|
|
247
|
-
if (!within(canonicalize(lexical))) throw invalidArgument("paths", `path escapes the workspace root: ${
|
|
228
|
+
if (!within(canonicalize(lexical))) throw invalidArgument("paths", `path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
|
|
248
229
|
return relative(cwd, lexical) || ".";
|
|
249
230
|
});
|
|
250
231
|
const doWrite = Boolean(args.write || args.unsafe);
|
|
251
232
|
const biomeCmd = Lint.Biome.findBiome();
|
|
252
233
|
if (!biomeCmd) throw new BiomeUnavailable({
|
|
253
|
-
message:
|
|
234
|
+
message: ToolFailure.message("Biome not found.", BIOME_REMEDIATION),
|
|
254
235
|
remediation: BIOME_REMEDIATION
|
|
255
236
|
});
|
|
256
237
|
const parts = biomeCmd.split(" ");
|
|
@@ -317,11 +298,11 @@ const BiomeCheckParams = Schema.Struct({
|
|
|
317
298
|
* the handler shells out directly and yields no service.
|
|
318
299
|
*/
|
|
319
300
|
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
|
|
301
|
+
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).",
|
|
321
302
|
parameters: BiomeCheckParams,
|
|
322
303
|
success: BiomeCheckResult,
|
|
323
304
|
failure: McpToolError
|
|
324
|
-
}).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false)
|
|
305
|
+
}).annotate(Tool.Title, "Biome check").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
|
|
325
306
|
const isMcpToolError = (u) => u instanceof InvalidArgument || u instanceof BiomeUnavailable || u instanceof BiomeFailed;
|
|
326
307
|
/**
|
|
327
308
|
* Wire handler: {@link runBiomeCheck} lifted into Effect. The typed members it
|
|
@@ -335,4 +316,4 @@ const handleBiomeCheck = (fallbackCwd, params) => Effect.tryPromise({
|
|
|
335
316
|
});
|
|
336
317
|
|
|
337
318
|
//#endregion
|
|
338
|
-
export {
|
|
319
|
+
export { BiomeCheckParams, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, biomeCheckTool, buildBiomeResult, handleBiomeCheck, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { McpToolError, mapEngineError } from "../errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
4
3
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
5
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -32,49 +31,6 @@ const ChangesetDepsDetectResult = Schema.Struct({
|
|
|
32
31
|
description: "Read-only per-package dependency diff (devDependencies retained). No files are written."
|
|
33
32
|
});
|
|
34
33
|
/**
|
|
35
|
-
* Render a repo-derived value as an inert markdown code span so a crafted path,
|
|
36
|
-
* package, or dependency name cannot inject markdown structure into the
|
|
37
|
-
* transcript an agent reads. Control characters (which would break table rows
|
|
38
|
-
* and headings) are flattened to spaces, table-cell pipes are escaped, and the
|
|
39
|
-
* span is fenced with a backtick run longer than any in the value — CommonMark
|
|
40
|
-
* forbids backslash-escaping a backtick inside a code span.
|
|
41
|
-
*/
|
|
42
|
-
const mdInline = (value) => {
|
|
43
|
-
const safe = value.replace(/\p{Cc}/gu, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
44
|
-
const longest = safe.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
|
|
45
|
-
const fence = "`".repeat(longest + 1);
|
|
46
|
-
const pad = safe.startsWith("`") || safe.endsWith("`") || safe.trim() === "" ? " " : "";
|
|
47
|
-
return `${fence}${pad}${safe}${pad}${fence}`;
|
|
48
|
-
};
|
|
49
|
-
/** Render the structured result as a markdown transcript. */
|
|
50
|
-
const renderMarkdown = (data) => {
|
|
51
|
-
const coexistingLines = data.coexisting.length === 0 ? [] : [
|
|
52
|
-
``,
|
|
53
|
-
`## Coexisting prose changesets (untouched by regen)`,
|
|
54
|
-
``,
|
|
55
|
-
...data.coexisting.map((c) => `- ${mdInline(c.file)} (${c.packages.map(mdInline).join(", ")})`)
|
|
56
|
-
];
|
|
57
|
-
if (data.packages.length === 0) return [
|
|
58
|
-
`# changeset deps detect — ${mdInline(data.root)}`,
|
|
59
|
-
``,
|
|
60
|
-
`No dependency changes detected.`
|
|
61
|
-
].concat(coexistingLines).join("\n").trimEnd();
|
|
62
|
-
const lines = [`# changeset deps detect — ${mdInline(data.root)}`, ``];
|
|
63
|
-
for (const pkg of data.packages) {
|
|
64
|
-
lines.push(`## ${mdInline(pkg.package)} — ${mdInline(pkg.relativePath)}`, ``);
|
|
65
|
-
lines.push(`| Dependency | Type | Action | From | To |`, `| --- | --- | --- | --- | --- |`);
|
|
66
|
-
for (const r of pkg.rows) lines.push(`| ${mdInline(r.dependency)} | ${r.type} | ${r.action} | ${mdInline(r.from)} | ${mdInline(r.to)} |`);
|
|
67
|
-
lines.push(``);
|
|
68
|
-
}
|
|
69
|
-
lines.push(...coexistingLines);
|
|
70
|
-
return lines.join("\n").trimEnd();
|
|
71
|
-
};
|
|
72
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
73
|
-
const ChangesetDepsDetectAsMarkdown = ChangesetDepsDetectResult.pipe(Schema.decodeTo(Schema.String, {
|
|
74
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
75
|
-
encode: SchemaGetter.forbidden(() => "ChangesetDepsDetectAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
76
|
-
}));
|
|
77
|
-
/**
|
|
78
34
|
* Effect handler: resolve the workspace root, then compute the cumulative
|
|
79
35
|
* dependency diff via {@link Changesets.DepsRegen.plan} with `includeDevDeps`
|
|
80
36
|
* so devDependency rows are retained. Maps `plan.toWrite` into the structured
|
|
@@ -117,14 +73,14 @@ const REMEDIATION = {
|
|
|
117
73
|
};
|
|
118
74
|
/** The `changeset_deps_detect` tool value. */
|
|
119
75
|
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.",
|
|
76
|
+
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. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
121
77
|
parameters: ChangesetDepsDetectParams,
|
|
122
78
|
success: ChangesetDepsDetectResult,
|
|
123
79
|
failure: McpToolError,
|
|
124
80
|
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)
|
|
81
|
+
}).annotate(Tool.Title, "Detect dependency changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
126
82
|
/** Wire handler: {@link changesetDepsDetect} with its error channel mapped onto {@link McpToolError}. */
|
|
127
83
|
const handleChangesetDepsDetect = (fallbackCwd, params) => changesetDepsDetect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
|
|
128
84
|
|
|
129
85
|
//#endregion
|
|
130
|
-
export {
|
|
86
|
+
export { ChangesetDepsDetectCoexisting, ChangesetDepsDetectPackage, ChangesetDepsDetectParams, ChangesetDepsDetectResult, changesetDepsDetect, changesetDepsDetectTool, handleChangesetDepsDetect };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { McpToolError, mapEngineError } from "../errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
4
3
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
5
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -29,51 +28,6 @@ const ChangesetDepsRegenResult = Schema.Struct({
|
|
|
29
28
|
description: "Regenerated pure-dependency changesets. Mutates .changeset/*.md unless dryRun is set."
|
|
30
29
|
});
|
|
31
30
|
/**
|
|
32
|
-
* Render a repo-derived value (path) as an inert markdown code span. Control
|
|
33
|
-
* characters are flattened to spaces and the span is fenced with a backtick run
|
|
34
|
-
* longer than any in the value — CommonMark forbids backslash-escaping a
|
|
35
|
-
* backtick inside a code span.
|
|
36
|
-
*/
|
|
37
|
-
const mdInline = (value) => {
|
|
38
|
-
const safe = value.replace(/\p{Cc}/gu, " ");
|
|
39
|
-
const longest = safe.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
|
|
40
|
-
const fence = "`".repeat(longest + 1);
|
|
41
|
-
const pad = safe.startsWith("`") || safe.endsWith("`") || safe.trim() === "" ? " " : "";
|
|
42
|
-
return `${fence}${pad}${safe}${pad}${fence}`;
|
|
43
|
-
};
|
|
44
|
-
/** Render the structured result as a markdown transcript. */
|
|
45
|
-
const renderMarkdown = (data) => {
|
|
46
|
-
const heading = `# changeset deps regen — ${mdInline(data.root)}${data.dryRun ? " (dry run)" : ""}`;
|
|
47
|
-
if (data.deleted.length === 0 && data.written.length === 0 && data.skippedMixed.length === 0 && data.coexisting.length === 0) return `${heading}\n\nNo dependency changes to regenerate.`;
|
|
48
|
-
const lines = [heading, ``];
|
|
49
|
-
const verb = data.dryRun ? "Would delete" : "Deleted";
|
|
50
|
-
if (data.deleted.length > 0) {
|
|
51
|
-
lines.push(`${verb} ${data.deleted.length} pure dependency changeset(s):`);
|
|
52
|
-
for (const file of data.deleted) lines.push(`- ${mdInline(file)}`);
|
|
53
|
-
lines.push(``);
|
|
54
|
-
}
|
|
55
|
-
if (data.written.length > 0) {
|
|
56
|
-
lines.push(`${data.dryRun ? "Would write" : "Wrote"} ${data.written.length} fresh dependency changeset(s):`);
|
|
57
|
-
for (const file of data.written) lines.push(`- ${mdInline(file)}`);
|
|
58
|
-
lines.push(``);
|
|
59
|
-
}
|
|
60
|
-
if (data.skippedMixed.length > 0) {
|
|
61
|
-
lines.push(`Skipped ${data.skippedMixed.length} mixed changeset(s):`);
|
|
62
|
-
for (const file of data.skippedMixed) lines.push(`- ${mdInline(file)}`);
|
|
63
|
-
lines.push(``);
|
|
64
|
-
}
|
|
65
|
-
if (data.coexisting.length > 0) {
|
|
66
|
-
lines.push(`Coexisting prose changeset(s) for in-scope packages, left untouched:`);
|
|
67
|
-
for (const entry of data.coexisting) lines.push(`- ${mdInline(entry.file)} (${entry.packages.map(mdInline).join(", ")})`);
|
|
68
|
-
}
|
|
69
|
-
return lines.join("\n").trimEnd();
|
|
70
|
-
};
|
|
71
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
72
|
-
const ChangesetDepsRegenAsMarkdown = ChangesetDepsRegenResult.pipe(Schema.decodeTo(Schema.String, {
|
|
73
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
74
|
-
encode: SchemaGetter.forbidden(() => "ChangesetDepsRegenAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
75
|
-
}));
|
|
76
|
-
/**
|
|
77
31
|
* Effect handler: resolve the workspace root, compute a {@link Changesets.RegenPlan}
|
|
78
32
|
* via {@link Changesets.DepsRegen.plan}, then — unless `dryRun` — apply it via
|
|
79
33
|
* `execute`. On a dry run the reported `deleted`/`written` reflect the plan's
|
|
@@ -128,14 +82,14 @@ const REMEDIATION = {
|
|
|
128
82
|
};
|
|
129
83
|
/** The `changeset_deps_regen` tool value. Mutating: not read-only, not idempotent. */
|
|
130
84
|
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.",
|
|
85
|
+
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. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
132
86
|
parameters: ChangesetDepsRegenParams,
|
|
133
87
|
success: ChangesetDepsRegenResult,
|
|
134
88
|
failure: McpToolError,
|
|
135
89
|
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)
|
|
90
|
+
}).annotate(Tool.Title, "Regenerate dependency changesets").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
|
|
137
91
|
/** Wire handler: {@link changesetDepsRegen} with its error channel mapped onto {@link McpToolError}. */
|
|
138
92
|
const handleChangesetDepsRegen = (fallbackCwd, params) => changesetDepsRegen(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
|
|
139
93
|
|
|
140
94
|
//#endregion
|
|
141
|
-
export { ChangesetCoexistingEntry,
|
|
95
|
+
export { ChangesetCoexistingEntry, ChangesetDepsRegenParams, ChangesetDepsRegenResult, changesetDepsRegen, changesetDepsRegenTool, handleChangesetDepsRegen };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { McpToolError, mapEngineError } from "../errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
4
3
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
5
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -32,79 +31,6 @@ const ChangesetInspectResult = Schema.Union([
|
|
|
32
31
|
description: "Read-only changeset analysis grouped by mode (branch | config | classify)."
|
|
33
32
|
});
|
|
34
33
|
/**
|
|
35
|
-
* Render a repo/config-derived value as an inert markdown code span. Escapes
|
|
36
|
-
* backticks and backslashes so a crafted filename or package name cannot inject
|
|
37
|
-
* markdown structure into the transcript that an agent reads.
|
|
38
|
-
*/
|
|
39
|
-
const mdInline = (value) => `\`${value.replace(/[`\\]/g, "\\$&")}\``;
|
|
40
|
-
/** Render the structured result as a markdown transcript. */
|
|
41
|
-
const renderMarkdown = (data) => {
|
|
42
|
-
switch (data.mode) {
|
|
43
|
-
case "branch": {
|
|
44
|
-
const r = data.result;
|
|
45
|
-
const lines = [
|
|
46
|
-
`# changeset branch analysis — base ${mdInline(r.baseBranch)}`,
|
|
47
|
-
``,
|
|
48
|
-
`merge base: ${mdInline(r.mergeBaseSha)}`,
|
|
49
|
-
``,
|
|
50
|
-
`## Packages affected`,
|
|
51
|
-
r.packagesAffected.map((p) => `- ${mdInline(p)}`).join("\n") || "(none)",
|
|
52
|
-
``,
|
|
53
|
-
`## Files`
|
|
54
|
-
];
|
|
55
|
-
for (const f of r.files) {
|
|
56
|
-
const owner = f.package ? mdInline(f.package) : mdInline("<unmapped>");
|
|
57
|
-
lines.push(`- ${mdInline(f.status)} ${mdInline(f.path)} -> ${owner}`);
|
|
58
|
-
}
|
|
59
|
-
if (r.unmappedFiles.length > 0) {
|
|
60
|
-
const hints = /* @__PURE__ */ new Map();
|
|
61
|
-
for (const f of r.files) if (typeof f.reason === "object" && f.reason !== null && f.reason.kind === "unmappedHint") hints.set(f.path, f.reason.hint);
|
|
62
|
-
lines.push(``, `## Unmapped (ask the user)`);
|
|
63
|
-
for (const p of r.unmappedFiles) {
|
|
64
|
-
const hint = hints.get(p);
|
|
65
|
-
lines.push(hint === void 0 ? `- ${mdInline(p)}` : `- ${mdInline(p)} — ${mdInline(hint)}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return lines.join("\n");
|
|
69
|
-
}
|
|
70
|
-
case "config": {
|
|
71
|
-
const r = data.result;
|
|
72
|
-
const lines = [
|
|
73
|
-
`# changeset config — ${mdInline(r.configPath)}`,
|
|
74
|
-
``,
|
|
75
|
-
`base branch: ${mdInline(r.baseBranch)}`,
|
|
76
|
-
`access: ${r.access}`,
|
|
77
|
-
`changelog: ${r.changelog ? mdInline(r.changelog) : "(none)"}`,
|
|
78
|
-
`ignored: ${r.ignore.map(mdInline).join(", ") || "(none)"}`,
|
|
79
|
-
``,
|
|
80
|
-
`## Packages`
|
|
81
|
-
];
|
|
82
|
-
for (const p of r.packages) {
|
|
83
|
-
lines.push(p.version === void 0 ? `### ${mdInline(p.name)}` : `### ${mdInline(p.name)} (${mdInline(p.version)})`, `- dir: ${mdInline(p.workspaceDir)}`);
|
|
84
|
-
if (p.additionalScopes.length > 0) lines.push(`- additionalScopes: ${p.additionalScopes.map(mdInline).join(", ")}`);
|
|
85
|
-
if (p.versionFiles.length > 0) lines.push(`- versionFiles: ${p.versionFiles.map((v) => mdInline(v.glob)).join(", ")}`);
|
|
86
|
-
}
|
|
87
|
-
if (r.packages.length === 0) lines.push("(none resolved)");
|
|
88
|
-
return lines.join("\n");
|
|
89
|
-
}
|
|
90
|
-
case "classify": {
|
|
91
|
-
const lines = [`# changeset classify`, ``];
|
|
92
|
-
for (const c of data.result) {
|
|
93
|
-
const owner = c.package ? mdInline(c.package) : mdInline("<unmapped>");
|
|
94
|
-
const hint = typeof c.reason === "object" && c.reason !== null && c.reason.kind === "unmappedHint" ? ` — ${mdInline(c.reason.hint)}` : "";
|
|
95
|
-
lines.push(`- ${mdInline(c.path)} -> ${owner}${hint}`);
|
|
96
|
-
}
|
|
97
|
-
if (data.result.length === 0) lines.push("(no paths)");
|
|
98
|
-
return lines.join("\n");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
103
|
-
const ChangesetInspectAsMarkdown = ChangesetInspectResult.pipe(Schema.decodeTo(Schema.String, {
|
|
104
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
105
|
-
encode: SchemaGetter.forbidden(() => "ChangesetInspectAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
106
|
-
}));
|
|
107
|
-
/**
|
|
108
34
|
* Effect handler: resolve the workspace root, then dispatch to the matching
|
|
109
35
|
* Changesets service keyed by `mode`. Mirrors `turboInspect`.
|
|
110
36
|
*/
|
|
@@ -144,7 +70,7 @@ const REMEDIATION = {
|
|
|
144
70
|
};
|
|
145
71
|
/** The `changeset_inspect` tool value. */
|
|
146
72
|
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.",
|
|
73
|
+
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. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
148
74
|
parameters: ChangesetInspectParams,
|
|
149
75
|
success: ChangesetInspectResult,
|
|
150
76
|
failure: McpToolError,
|
|
@@ -153,9 +79,9 @@ const changesetInspectTool = Tool.make("changeset_inspect", {
|
|
|
153
79
|
Changesets.ConfigInspector,
|
|
154
80
|
WorkspaceRoot
|
|
155
81
|
]
|
|
156
|
-
}).annotate(Tool.Title, "Inspect changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false)
|
|
82
|
+
}).annotate(Tool.Title, "Inspect changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
157
83
|
/** Wire handler: {@link changesetInspect} with its error channel mapped onto {@link McpToolError}. */
|
|
158
84
|
const handleChangesetInspect = (fallbackCwd, params) => changesetInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
|
|
159
85
|
|
|
160
86
|
//#endregion
|
|
161
|
-
export { ChangesetBranchResult, ChangesetClassifyResult, ChangesetConfigResult,
|
|
87
|
+
export { ChangesetBranchResult, ChangesetClassifyResult, ChangesetConfigResult, ChangesetInspectParams, ChangesetInspectResult, changesetInspect, changesetInspectTool, handleChangesetInspect };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { McpToolError, mapEngineError } from "../errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
4
3
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
5
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -12,28 +11,6 @@ const ChangesetPreviewResult = Changesets.ChangesetPreviewSchema.annotate({
|
|
|
12
11
|
title: "changeset_preview result",
|
|
13
12
|
description: "Read-only preview of the next release: version bumps + rendered CHANGELOG blocks."
|
|
14
13
|
});
|
|
15
|
-
/** Render a value as an inert markdown code span (escapes backticks/backslashes). */
|
|
16
|
-
const mdInline = (value) => `\`${value.replace(/[`\\]/g, "\\$&")}\``;
|
|
17
|
-
/** Render the structured preview as a markdown transcript. */
|
|
18
|
-
const renderMarkdown = (data) => {
|
|
19
|
-
if (data.releases.length === 0) return "# changeset preview\n\nNo pending changesets.";
|
|
20
|
-
const lines = [
|
|
21
|
-
`# changeset preview${data.preMode ? ` (pre: ${data.preMode})` : ""}`,
|
|
22
|
-
``,
|
|
23
|
-
`## Version bumps`,
|
|
24
|
-
``
|
|
25
|
-
];
|
|
26
|
-
lines.push(`| Package | Old | New | Bump |`, `| --- | --- | --- | --- |`);
|
|
27
|
-
for (const r of data.releases) lines.push(`| ${mdInline(r.name)} | ${r.oldVersion} | ${r.newVersion} | ${r.type} |`);
|
|
28
|
-
lines.push(``, `## Release notes`, ``);
|
|
29
|
-
for (const r of data.releases) lines.push(`### ${mdInline(r.name)}`, ``, r.changelogEntry, ``);
|
|
30
|
-
return lines.join("\n").trimEnd();
|
|
31
|
-
};
|
|
32
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
33
|
-
const ChangesetPreviewAsMarkdown = ChangesetPreviewResult.pipe(Schema.decodeTo(Schema.String, {
|
|
34
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
35
|
-
encode: SchemaGetter.forbidden(() => "ChangesetPreviewAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
36
|
-
}));
|
|
37
14
|
/**
|
|
38
15
|
* Effect handler: resolve the workspace root, then render the preview via
|
|
39
16
|
* ReleasePlanner. Mirrors `changesetInspect`.
|
|
@@ -50,14 +27,14 @@ const REMEDIATION = {
|
|
|
50
27
|
};
|
|
51
28
|
/** The `changeset_preview` tool value. */
|
|
52
29
|
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.",
|
|
30
|
+
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. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
54
31
|
parameters: ChangesetPreviewParams,
|
|
55
32
|
success: ChangesetPreviewResult,
|
|
56
33
|
failure: McpToolError,
|
|
57
34
|
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)
|
|
35
|
+
}).annotate(Tool.Title, "Preview the next release").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
59
36
|
/** Wire handler: {@link changesetPreview} with its error channel mapped onto {@link McpToolError}. */
|
|
60
37
|
const handleChangesetPreview = (fallbackCwd, params) => changesetPreview(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
|
|
61
38
|
|
|
62
39
|
//#endregion
|
|
63
|
-
export {
|
|
40
|
+
export { ChangesetPreviewParams, ChangesetPreviewResult, changesetPreview, changesetPreviewTool, handleChangesetPreview };
|