@savvy-web/mcp 2.7.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/bin/savvy-mcp.js +4 -28
- package/errors.js +181 -0
- package/index.d.ts +695 -18
- package/index.js +5 -2
- package/internal/project-root.js +25 -0
- package/main.d.ts +25 -0
- package/main.js +42 -0
- package/markdown.js +32 -0
- package/package.json +10 -7
- package/runtime.js +3 -3
- package/server.js +119 -234
- package/toolkit.js +44 -0
- package/tools/biome-check.js +64 -11
- package/tools/changeset-deps-detect.js +27 -2
- package/tools/changeset-deps-regen.js +28 -2
- package/tools/changeset-inspect.js +34 -2
- package/tools/changeset-preview.js +21 -2
- package/tools/changeset-validate.js +28 -2
- package/tools/repos-inspect.js +38 -2
- package/tools/repos-manage.js +58 -2
- package/tools/turbo-inspect.js +27 -2
- package/tools/workspace-info.js +25 -2
- package/version.js +1 -1
- package/schema/effect-to-zod.js +0 -205
package/tools/biome-check.js
CHANGED
|
@@ -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 {
|
|
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
|
|
217
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 };
|
package/tools/repos-inspect.js
CHANGED
|
@@ -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 {
|
|
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 };
|
package/tools/repos-manage.js
CHANGED
|
@@ -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 {
|
|
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 };
|
package/tools/turbo-inspect.js
CHANGED
|
@@ -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 {
|
|
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 };
|
package/tools/workspace-info.js
CHANGED
|
@@ -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 {
|
|
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 };
|