@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { McpToolError, invalidArgument, mapEngineError
|
|
2
|
-
import {
|
|
3
|
-
import { Data, Effect, Schema
|
|
1
|
+
import { McpToolError, invalidArgument, mapEngineError } from "../errors.js";
|
|
2
|
+
import { ToolFailure } from "@effected/mcp";
|
|
3
|
+
import { Data, Effect, Schema } from "effect";
|
|
4
4
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
5
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
6
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -36,29 +36,6 @@ const ChangesetValidateResult = Schema.Struct({
|
|
|
36
36
|
description: "Read-only validation of changeset files against the section-aware rules."
|
|
37
37
|
});
|
|
38
38
|
/**
|
|
39
|
-
* Render a repo/lint-derived value as an inert markdown code span. Escapes
|
|
40
|
-
* backticks and backslashes so a crafted filename or message cannot inject
|
|
41
|
-
* markdown structure into the transcript that an agent reads.
|
|
42
|
-
*/
|
|
43
|
-
const mdInline = (value) => `\`${value.replace(/[`\\]/g, "\\$&")}\``;
|
|
44
|
-
/** Render the structured result as a markdown transcript. */
|
|
45
|
-
const renderMarkdown = (data) => {
|
|
46
|
-
if (data.ok) return `# changeset validate — ${mdInline(data.dir)}\n\nNo changeset issues found.`;
|
|
47
|
-
const lines = [
|
|
48
|
-
`# changeset validate — ${mdInline(data.dir)}`,
|
|
49
|
-
``,
|
|
50
|
-
`${data.errorCount} issue(s):`,
|
|
51
|
-
``
|
|
52
|
-
];
|
|
53
|
-
for (const m of data.messages) lines.push(`- ${mdInline(`${m.file}:${m.line}:${m.column}`)} ${mdInline(m.rule)} — ${m.message}`);
|
|
54
|
-
return lines.join("\n");
|
|
55
|
-
};
|
|
56
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
57
|
-
const ChangesetValidateAsMarkdown = ChangesetValidateResult.pipe(Schema.decodeTo(Schema.String, {
|
|
58
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
59
|
-
encode: SchemaGetter.forbidden(() => "ChangesetValidateAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
60
|
-
}));
|
|
61
|
-
/**
|
|
62
39
|
* Effect handler: resolve the workspace root, then validate the changeset
|
|
63
40
|
* directory via the pure {@link Changesets.ChangesetLinter.validate}. The
|
|
64
41
|
* synchronous call is wrapped in {@link Effect.try} so a thrown error (e.g. a
|
|
@@ -90,12 +67,12 @@ const ChangesetValidateParams = Schema.Struct({
|
|
|
90
67
|
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
68
|
/** The `changeset_validate` tool value. */
|
|
92
69
|
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.",
|
|
70
|
+
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. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
94
71
|
parameters: ChangesetValidateParams,
|
|
95
72
|
success: ChangesetValidateResult,
|
|
96
73
|
failure: McpToolError,
|
|
97
74
|
dependencies: [WorkspaceRoot]
|
|
98
|
-
}).annotate(Tool.Title, "Validate changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false)
|
|
75
|
+
}).annotate(Tool.Title, "Validate changesets").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
99
76
|
/**
|
|
100
77
|
* Wire handler: {@link changesetValidate} with its error channel mapped onto
|
|
101
78
|
* {@link McpToolError}. The typed {@link ChangesetValidateError} (a thrown
|
|
@@ -103,8 +80,8 @@ const changesetValidateTool = Tool.make("changeset_validate", {
|
|
|
103
80
|
* becomes {@link InvalidArgument} naming `dir`; the echoed directory is
|
|
104
81
|
* truncated.
|
|
105
82
|
*/
|
|
106
|
-
const handleChangesetValidate = (fallbackCwd, params) => changesetValidate(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "ChangesetValidateError" ? invalidArgument("dir", `Changeset directory "${
|
|
107
|
-
const describeCause = (cause) => cause instanceof Error ?
|
|
83
|
+
const handleChangesetValidate = (fallbackCwd, params) => changesetValidate(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "ChangesetValidateError" ? invalidArgument("dir", `Changeset directory "${ToolFailure.truncate(error.dir)}" could not be validated: ${describeCause(error.cause)}`, DIR_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, DIR_REMEDIATION)(error)));
|
|
84
|
+
const describeCause = (cause) => cause instanceof Error ? ToolFailure.truncate(cause.message) : ToolFailure.truncate(String(cause));
|
|
108
85
|
|
|
109
86
|
//#endregion
|
|
110
|
-
export { ChangesetLintMessage,
|
|
87
|
+
export { ChangesetLintMessage, ChangesetValidateError, ChangesetValidateParams, ChangesetValidateResult, changesetValidate, changesetValidateTool, handleChangesetValidate };
|
package/tools/repos-inspect.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { McpToolError, mapEngineError } from "../errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { mdInline } from "./md-inline.js";
|
|
4
|
-
import { Effect, FileSystem, Option, Path, Result, Schema, SchemaGetter } from "effect";
|
|
2
|
+
import { Effect, FileSystem, Option, Path, Result, Schema } from "effect";
|
|
5
3
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
6
4
|
import { Repos } from "@savvy-web/silk-effects";
|
|
7
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -11,8 +9,7 @@ import { Gitmodules } from "@effected/git";
|
|
|
11
9
|
/**
|
|
12
10
|
* The `repos_inspect` MCP tool: a discriminated-union result keyed by `mode`
|
|
13
11
|
* (status | config), each variant embedding the corresponding resolved-output
|
|
14
|
-
* schema from silk-effects' Repos namespace
|
|
15
|
-
* transform. Read-only.
|
|
12
|
+
* schema from silk-effects' Repos namespace. Read-only.
|
|
16
13
|
*
|
|
17
14
|
* @packageDocumentation
|
|
18
15
|
*/
|
|
@@ -64,81 +61,6 @@ const ReposInspectResult = Schema.Union([
|
|
|
64
61
|
title: "repos_inspect result",
|
|
65
62
|
description: "Drift report (status), the parsed manifest (config), the five-authority reconciliation report (drift), or the decoded .gitmodules sections (gitmodules)."
|
|
66
63
|
});
|
|
67
|
-
/** Render the structured result as a markdown transcript. */
|
|
68
|
-
const renderMarkdown = (data) => {
|
|
69
|
-
switch (data.mode) {
|
|
70
|
-
case "status": {
|
|
71
|
-
const r = data.result;
|
|
72
|
-
const lines = [
|
|
73
|
-
`# repos status`,
|
|
74
|
-
``,
|
|
75
|
-
`clean: ${r.clean}`,
|
|
76
|
-
``,
|
|
77
|
-
`## Repos`
|
|
78
|
-
];
|
|
79
|
-
for (const entry of r.repos) {
|
|
80
|
-
lines.push(`### ${mdInline(entry.name)}`, `- ref: ${mdInline(entry.ref)}`, `- purpose: ${mdInline(entry.purpose)}`, `- present: ${entry.present}`, `- stagedCommit: ${entry.stagedCommit ? mdInline(entry.stagedCommit) : "(none)"}`, `- committedCommit: ${entry.committedCommit ? mdInline(entry.committedCommit) : "(none)"}`, `- checkedOutCommit: ${entry.checkedOutCommit ? mdInline(entry.checkedOutCommit) : "(none)"}`, `- dirty: ${entry.dirty}`);
|
|
81
|
-
if (entry.staleNoteIds.length > 0) lines.push(`- staleNoteIds: ${entry.staleNoteIds.map(mdInline).join(", ")}`);
|
|
82
|
-
}
|
|
83
|
-
if (r.repos.length === 0) lines.push("(none)");
|
|
84
|
-
return lines.join("\n");
|
|
85
|
-
}
|
|
86
|
-
case "config": {
|
|
87
|
-
const r = data.result;
|
|
88
|
-
const lines = [
|
|
89
|
-
`# repos config`,
|
|
90
|
-
``,
|
|
91
|
-
`## Repos`
|
|
92
|
-
];
|
|
93
|
-
for (const [name, entry] of Object.entries(r.repos)) {
|
|
94
|
-
lines.push(`### ${mdInline(name)}`, `- url: ${mdInline(entry.url)}`, `- ref: ${mdInline(entry.ref)}`, `- purpose: ${mdInline(entry.purpose)}`);
|
|
95
|
-
if (entry.sparse && entry.sparse.length > 0) lines.push(`- sparse: ${entry.sparse.map(mdInline).join(", ")}`);
|
|
96
|
-
if (entry.orientation) {
|
|
97
|
-
const o = entry.orientation;
|
|
98
|
-
if (o.layout) lines.push(`- layout: ${mdInline(o.layout)}`);
|
|
99
|
-
if (o.startHere) lines.push(`- startHere: ${mdInline(o.startHere)}`);
|
|
100
|
-
if (o.keyPaths) {
|
|
101
|
-
lines.push(`- keyPaths:`);
|
|
102
|
-
for (const [key, value] of Object.entries(o.keyPaths)) lines.push(` - ${mdInline(key)}: ${mdInline(value)}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (entry.notes && entry.notes.length > 0) {
|
|
106
|
-
lines.push(`- notes:`);
|
|
107
|
-
for (const note of entry.notes) lines.push(` - ${mdInline(note.id)} (${mdInline(note.date)}, ref ${mdInline(note.ref)}): ${mdInline(note.note)}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (Object.keys(r.repos).length === 0) lines.push("(none)");
|
|
111
|
-
return lines.join("\n");
|
|
112
|
-
}
|
|
113
|
-
case "drift": {
|
|
114
|
-
const r = data.report;
|
|
115
|
-
const lines = [
|
|
116
|
-
`# repos drift`,
|
|
117
|
-
``,
|
|
118
|
-
`clean: ${r.clean}`,
|
|
119
|
-
``,
|
|
120
|
-
`| name | kind | detail |`,
|
|
121
|
-
`| --- | --- | --- |`
|
|
122
|
-
];
|
|
123
|
-
for (const d of r.drifts) lines.push(`| ${mdInline(d.name)} | ${mdInline(d.kind)} | ${mdInline(d.detail)} |`);
|
|
124
|
-
if (r.drifts.length === 0) lines.push(``, `(none)`);
|
|
125
|
-
return lines.join("\n");
|
|
126
|
-
}
|
|
127
|
-
case "gitmodules": {
|
|
128
|
-
const lines = [`# repos gitmodules`, ``];
|
|
129
|
-
if (data.parseError) lines.push(`parse error: ${mdInline(data.parseError)}`, ``);
|
|
130
|
-
lines.push(`| name | path | url | branch | shallow |`, `| --- | --- | --- | --- | --- |`);
|
|
131
|
-
for (const entry of data.entries) lines.push(`| ${mdInline(entry.name)} | ${mdInline(entry.path)} | ${mdInline(entry.url)} | ${entry.branch === void 0 ? "(none)" : mdInline(entry.branch)} | ${entry.shallow === void 0 ? "(unset)" : entry.shallow} |`);
|
|
132
|
-
if (data.entries.length === 0) lines.push(``, `(none)`);
|
|
133
|
-
return lines.join("\n");
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
138
|
-
const ReposInspectAsMarkdown = ReposInspectResult.pipe(Schema.decodeTo(Schema.String, {
|
|
139
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
140
|
-
encode: SchemaGetter.forbidden(() => "ReposInspectAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
141
|
-
}));
|
|
142
64
|
/**
|
|
143
65
|
* Effect handler: resolve the workspace root, then dispatch to the matching
|
|
144
66
|
* Repos service keyed by `mode`. Mirrors `changesetInspect`.
|
|
@@ -207,7 +129,7 @@ const ReposInspectParams = Schema.Struct({
|
|
|
207
129
|
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
130
|
/** The `repos_inspect` tool value. */
|
|
209
131
|
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.",
|
|
132
|
+
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. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
211
133
|
parameters: ReposInspectParams,
|
|
212
134
|
success: ReposInspectResult,
|
|
213
135
|
failure: McpToolError,
|
|
@@ -219,9 +141,9 @@ const reposInspectTool = Tool.make("repos_inspect", {
|
|
|
219
141
|
FileSystem.FileSystem,
|
|
220
142
|
Path.Path
|
|
221
143
|
]
|
|
222
|
-
}).annotate(Tool.Title, "Inspect vendored repos").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false)
|
|
144
|
+
}).annotate(Tool.Title, "Inspect vendored repos").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
223
145
|
/** Wire handler: {@link reposInspect} with its error channel mapped onto {@link McpToolError}. */
|
|
224
146
|
const handleReposInspect = (fallbackCwd, params) => reposInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
|
|
225
147
|
|
|
226
148
|
//#endregion
|
|
227
|
-
export { ReposConfigResult, ReposDriftResult, ReposGitmodulesResult,
|
|
149
|
+
export { ReposConfigResult, ReposDriftResult, ReposGitmodulesResult, ReposInspectMode, ReposInspectParams, ReposInspectResult, ReposStatusResult, handleReposInspect, reposInspect, reposInspectTool };
|
package/tools/repos-manage.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
1
|
+
import { McpToolError, invalidArgument, mapEngineError } from "../errors.js";
|
|
2
|
+
import { ToolFailure } from "@effected/mcp";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
5
4
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
6
5
|
import { Repos } from "@savvy-web/silk-effects";
|
|
7
6
|
import { Tool } from "effect/unstable/ai";
|
|
8
7
|
|
|
9
8
|
//#region src/tools/repos-manage.ts
|
|
9
|
+
/**
|
|
10
|
+
* The `repos_manage` MCP tool: one action-discriminated mutating tool
|
|
11
|
+
* covering `sync`, `pin`, `add`, `note`, `remove`, `rename`, `restore`, and
|
|
12
|
+
* `deregister` against the vendored `.repos/` submodules. The wire schema is flat (no
|
|
13
|
+
* `oneOf`); the handler maps it into an internal `Schema.TaggedStruct`
|
|
14
|
+
* request union that names the missing field per action on decode failure.
|
|
15
|
+
* Mutating — no `readOnlyHint`.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
10
19
|
/** `sync` has no extra fields. */
|
|
11
20
|
const SyncRequest = Schema.TaggedStruct("sync", {});
|
|
12
21
|
/** `pin` requires both `name` and `ref`. */
|
|
@@ -129,139 +138,6 @@ const ReposManageResult = Schema.Union([
|
|
|
129
138
|
title: "repos_manage result",
|
|
130
139
|
description: "Result of a mutating repos action: sync, pin, add, note, remove, rename, restore, or deregister."
|
|
131
140
|
});
|
|
132
|
-
/** Render the structured result as a markdown transcript. */
|
|
133
|
-
const renderMarkdown = (data) => {
|
|
134
|
-
switch (data.action) {
|
|
135
|
-
case "sync": {
|
|
136
|
-
const r = data.result;
|
|
137
|
-
const section = (title, names) => [`## ${title}`, ...names.length > 0 ? names.map((name) => `- ${mdInline(name)}`) : ["(none)"]];
|
|
138
|
-
return [
|
|
139
|
-
`# repos sync`,
|
|
140
|
-
``,
|
|
141
|
-
...section("Initialized", r.initialized),
|
|
142
|
-
``,
|
|
143
|
-
...section("Sparse applied", r.sparseApplied),
|
|
144
|
-
``,
|
|
145
|
-
...section("Up to date", r.upToDate),
|
|
146
|
-
``,
|
|
147
|
-
...section("Cleared locks", r.clearedLocks),
|
|
148
|
-
``,
|
|
149
|
-
...section("URL synced", r.urlSynced),
|
|
150
|
-
``,
|
|
151
|
-
...section("Registered", r.registered),
|
|
152
|
-
``,
|
|
153
|
-
...section("Boundary marked", r.boundaryMarked)
|
|
154
|
-
].join("\n");
|
|
155
|
-
}
|
|
156
|
-
case "pin": {
|
|
157
|
-
const r = data.result;
|
|
158
|
-
const lines = [
|
|
159
|
-
`# repos pin — ${mdInline(r.name)}`,
|
|
160
|
-
``,
|
|
161
|
-
`ref: ${mdInline(r.ref)}`,
|
|
162
|
-
`oldCommit: ${r.oldCommit ? mdInline(r.oldCommit) : "(none)"}`,
|
|
163
|
-
`newCommit: ${mdInline(r.newCommit)}`,
|
|
164
|
-
``,
|
|
165
|
-
`## Commit message`,
|
|
166
|
-
``,
|
|
167
|
-
mdInline(r.commitMessage),
|
|
168
|
-
``,
|
|
169
|
-
`## Stale notes`,
|
|
170
|
-
``
|
|
171
|
-
];
|
|
172
|
-
if (r.staleNoteIds.length > 0) lines.push(`These notes reference a ref other than the new pin and should be reviewed before committing:`, ...r.staleNoteIds.map((id) => `- ${mdInline(id)}`));
|
|
173
|
-
else lines.push("(none)");
|
|
174
|
-
lines.push(``, `REVIEW AND COMMIT: stage the updated manifest and submodule gitlink, then commit using the message above.`);
|
|
175
|
-
return lines.join("\n");
|
|
176
|
-
}
|
|
177
|
-
case "add": {
|
|
178
|
-
const r = data.result;
|
|
179
|
-
return [
|
|
180
|
-
`# repos add — ${mdInline(r.name)}`,
|
|
181
|
-
``,
|
|
182
|
-
`ref: ${mdInline(r.ref)}`,
|
|
183
|
-
`path: ${mdInline(r.path)}`
|
|
184
|
-
].join("\n");
|
|
185
|
-
}
|
|
186
|
-
case "note": {
|
|
187
|
-
const r = data.result;
|
|
188
|
-
return [
|
|
189
|
-
`# repos note — ${mdInline(r.name)}`,
|
|
190
|
-
``,
|
|
191
|
-
`op: ${mdInline(r.op)}`,
|
|
192
|
-
`id: ${mdInline(r.id)}`,
|
|
193
|
-
`noteCount: ${r.noteCount}`
|
|
194
|
-
].join("\n");
|
|
195
|
-
}
|
|
196
|
-
case "remove": {
|
|
197
|
-
const r = data.result;
|
|
198
|
-
const lines = [
|
|
199
|
-
`# repos remove — ${mdInline(r.name)}`,
|
|
200
|
-
``,
|
|
201
|
-
`path: ${mdInline(r.path)}`,
|
|
202
|
-
``,
|
|
203
|
-
`## Commit message`,
|
|
204
|
-
``,
|
|
205
|
-
mdInline(r.commitMessage),
|
|
206
|
-
``,
|
|
207
|
-
`## Removed notes`,
|
|
208
|
-
``
|
|
209
|
-
];
|
|
210
|
-
if (r.removedNotes.length > 0) lines.push(`Promote any durable ones elsewhere before committing:`, ...r.removedNotes.map((note) => `- ${mdInline(note.id)} (${mdInline(note.ref)}): ${mdInline(note.note)}`));
|
|
211
|
-
else lines.push("(none)");
|
|
212
|
-
if (r.removedEntry.orientation) lines.push(``, `## Removed orientation`, ``, `\`add\` does NOT restore this. If you are re-vendoring ${mdInline(r.name)}, pass it back as the \`orientation\` argument:`, ``, "```json", JSON.stringify(r.removedEntry.orientation, null, 2), "```");
|
|
213
|
-
lines.push(``, `REVIEW AND COMMIT: the manifest, .gitmodules, and gitlink removal are already staged — review and commit using the message above.`);
|
|
214
|
-
return lines.join("\n");
|
|
215
|
-
}
|
|
216
|
-
case "rename": {
|
|
217
|
-
const r = data.result;
|
|
218
|
-
return [
|
|
219
|
-
`# repos rename — ${mdInline(r.oldName)} → ${mdInline(r.newName)}`,
|
|
220
|
-
``,
|
|
221
|
-
`path: ${mdInline(r.path)}`,
|
|
222
|
-
``,
|
|
223
|
-
`## Commit message`,
|
|
224
|
-
``,
|
|
225
|
-
mdInline(r.commitMessage),
|
|
226
|
-
``,
|
|
227
|
-
`REVIEW AND COMMIT: the moved worktree, .gitmodules section, and manifest key are already staged — review and commit using the message above.`
|
|
228
|
-
].join("\n");
|
|
229
|
-
}
|
|
230
|
-
case "restore": {
|
|
231
|
-
const r = data.result;
|
|
232
|
-
const lines = [
|
|
233
|
-
`# repos restore`,
|
|
234
|
-
``,
|
|
235
|
-
`## Restored`,
|
|
236
|
-
``
|
|
237
|
-
];
|
|
238
|
-
if (r.restored.length > 0) lines.push(`DESTRUCTIVE: any uncommitted worktree edits and untracked files in these repos were discarded — the working tree was hard-reset to the commit below and sparse paths re-applied.`, ``, ...r.restored.map((entry) => `- ${mdInline(entry.name)} → ${mdInline(entry.commit)}`));
|
|
239
|
-
else lines.push("(none)");
|
|
240
|
-
lines.push(``, `## Skipped (clean)`, ``);
|
|
241
|
-
if (r.skippedClean.length > 0) lines.push(...r.skippedClean.map((name) => `- ${mdInline(name)}`));
|
|
242
|
-
else lines.push("(none)");
|
|
243
|
-
if (r.stillDirty.length > 0) lines.push(``, `## Still dirty — RESTORE DID NOT FULLY SUCCEED`, ``, `The reset ran but these worktrees remain dirty, so treat the restore as incomplete rather than done:`, ``, ...r.stillDirty.map((name) => `- ${mdInline(name)}`), ``, `Run repos_inspect (mode: drift) — a nestedSubmoduleDivergence or a permission problem is the usual cause.`);
|
|
244
|
-
return lines.join("\n");
|
|
245
|
-
}
|
|
246
|
-
case "deregister": {
|
|
247
|
-
const r = data.result;
|
|
248
|
-
return [
|
|
249
|
-
`# repos deregister — ${mdInline(r.section)}`,
|
|
250
|
-
``,
|
|
251
|
-
`Removed the stale ${mdInline(`submodule.${r.section}`)} section from the superproject's LOCAL git config. Keys cleared:`,
|
|
252
|
-
``,
|
|
253
|
-
...r.removedKeys.map((key) => `- ${mdInline(key)}`),
|
|
254
|
-
``,
|
|
255
|
-
`Local config only — nothing is staged and there is nothing to commit.`
|
|
256
|
-
].join("\n");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
261
|
-
const ReposManageAsMarkdown = ReposManageResult.pipe(Schema.decodeTo(Schema.String, {
|
|
262
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
263
|
-
encode: SchemaGetter.forbidden(() => "ReposManageAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
264
|
-
}));
|
|
265
141
|
/**
|
|
266
142
|
* Effect handler: resolve the workspace root, decode the flat wire args into
|
|
267
143
|
* the internal per-action request (naming the missing field on failure), then
|
|
@@ -368,21 +244,21 @@ const REMEDIATION = {
|
|
|
368
244
|
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
245
|
/** The `repos_manage` tool value. Mutating: not read-only, not idempotent. */
|
|
370
246
|
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
|
|
247
|
+
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 surfaces commitMessage and staleNoteIds — review and commit after pinning. The remove result 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 surfaces commitMessage — review and commit after renaming. The restore result names exactly what was discarded. The deregister result lists the config keys the removed section carried — nothing to commit afterwards. Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
372
248
|
parameters: ReposManageParams,
|
|
373
249
|
success: ReposManageResult,
|
|
374
250
|
failure: McpToolError,
|
|
375
251
|
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)
|
|
252
|
+
}).annotate(Tool.Title, "Manage vendored repos").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false).annotate(Tool.OpenWorld, false);
|
|
377
253
|
/**
|
|
378
254
|
* Wire handler: {@link reposManage} with its error channel mapped onto
|
|
379
255
|
* {@link McpToolError}. The per-action request decode failure (`SchemaError`,
|
|
380
256
|
* which names the missing field, and echoes the decoded value) is an argument
|
|
381
257
|
* problem and becomes {@link InvalidArgument} keyed on `action`, its text
|
|
382
|
-
* truncated at
|
|
258
|
+
* truncated at `ToolFailure.ENGINE_ECHO_LIMIT`; the engine's own errors go
|
|
383
259
|
* through {@link mapEngineError}.
|
|
384
260
|
*/
|
|
385
|
-
const handleReposManage = (fallbackCwd, params) => reposManage(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "SchemaError" ? invalidArgument("action", `repos_manage ${params.action}: ${
|
|
261
|
+
const handleReposManage = (fallbackCwd, params) => reposManage(params, fallbackCwd).pipe(Effect.mapError((error) => error._tag === "SchemaError" ? invalidArgument("action", `repos_manage ${params.action}: ${ToolFailure.truncate(error.message, ToolFailure.ENGINE_ECHO_LIMIT)}`, REQUEST_REMEDIATION) : mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)(error)));
|
|
386
262
|
|
|
387
263
|
//#endregion
|
|
388
|
-
export { ReposManageAddResult,
|
|
264
|
+
export { ReposManageAddResult, ReposManageDeregisterResult, ReposManageNoteResult, ReposManageParams, ReposManagePinResult, ReposManageRemoveResult, ReposManageRenameResult, ReposManageRestoreResult, ReposManageResult, ReposManageSyncResult, handleReposManage, reposManage, reposManageTool };
|
package/tools/turbo-inspect.js
CHANGED
|
@@ -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 { Turbo } from "@savvy-web/silk-effects";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -31,59 +30,6 @@ const TurboInspectResult = Schema.Union([
|
|
|
31
30
|
title: "turbo_inspect result",
|
|
32
31
|
description: "Read-only Turborepo inspection grouped by mode (cache | graph | affected)."
|
|
33
32
|
});
|
|
34
|
-
/** Render the structured result as a markdown transcript. */
|
|
35
|
-
const renderMarkdown = (data) => {
|
|
36
|
-
switch (data.mode) {
|
|
37
|
-
case "cache": {
|
|
38
|
-
const r = data.result;
|
|
39
|
-
const lines = [
|
|
40
|
-
`# turbo cache — ${r.task}`,
|
|
41
|
-
``,
|
|
42
|
-
`**${r.hits}/${r.totalTasks} cached**, ${r.misses} miss(es).`,
|
|
43
|
-
``,
|
|
44
|
-
`## Global hash`,
|
|
45
|
-
`- rootKey: \`${r.global.rootKey}\``,
|
|
46
|
-
`- global files: ${r.global.globalFileCount}`,
|
|
47
|
-
`- external deps hash: \`${r.global.externalDependenciesHash}\``,
|
|
48
|
-
`- internal deps hash: \`${r.global.internalDependenciesHash}\``,
|
|
49
|
-
`- global env: ${r.global.globalEnvVars.join(", ") || "(none)"}`
|
|
50
|
-
];
|
|
51
|
-
if (r.explanations.length > 0) {
|
|
52
|
-
lines.push(``, `## Misses`);
|
|
53
|
-
for (const m of r.explanations) lines.push(`### ${m.package} (\`${m.taskId}\`)`, `- hash: \`${m.hash}\``, `- input files: ${m.inputFileCount}`, `- hashed env: ${m.hashedEnvVars.join(", ") || "(none)"}`, `- external deps hash: \`${m.externalDependenciesHash}\``, `- depends on: ${m.dependsOn.join(", ") || "(none)"}`);
|
|
54
|
-
}
|
|
55
|
-
return lines.join("\n");
|
|
56
|
-
}
|
|
57
|
-
case "graph": {
|
|
58
|
-
const r = data.result;
|
|
59
|
-
return [
|
|
60
|
-
`# turbo task graph${r.task ? ` — ${r.task}` : ""}`,
|
|
61
|
-
``,
|
|
62
|
-
`${r.nodeCount} task node(s).`,
|
|
63
|
-
``,
|
|
64
|
-
`## Critical path`,
|
|
65
|
-
r.criticalPath.map((id, i) => `${i + 1}. \`${id}\``).join("\n") || "(empty)"
|
|
66
|
-
].join("\n");
|
|
67
|
-
}
|
|
68
|
-
case "affected": {
|
|
69
|
-
const r = data.result;
|
|
70
|
-
return [
|
|
71
|
-
`# turbo affected — base ${r.base}`,
|
|
72
|
-
``,
|
|
73
|
-
`## Changed packages`,
|
|
74
|
-
r.packages.map((p) => `- ${p}`).join("\n") || "(none)",
|
|
75
|
-
``,
|
|
76
|
-
`## Dependents`,
|
|
77
|
-
r.dependents.map((p) => `- ${p}`).join("\n") || "(none)"
|
|
78
|
-
].join("\n");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
83
|
-
const TurboInspectAsMarkdown = TurboInspectResult.pipe(Schema.decodeTo(Schema.String, {
|
|
84
|
-
decode: SchemaGetter.transform(renderMarkdown),
|
|
85
|
-
encode: SchemaGetter.forbidden(() => "TurboInspectAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
86
|
-
}));
|
|
87
33
|
/**
|
|
88
34
|
* Effect handler: resolve the workspace root by walking up from the requested
|
|
89
35
|
* directory, then dispatch to the matching {@link Turbo.TurboInspector} method
|
|
@@ -121,14 +67,14 @@ const TurboInspectParams = Schema.Struct({
|
|
|
121
67
|
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
68
|
/** The `turbo_inspect` tool value. */
|
|
123
69
|
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).",
|
|
70
|
+
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). Returns a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
125
71
|
parameters: TurboInspectParams,
|
|
126
72
|
success: TurboInspectResult,
|
|
127
73
|
failure: McpToolError,
|
|
128
74
|
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)
|
|
75
|
+
}).annotate(Tool.Title, "Inspect Turborepo").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
130
76
|
/** Wire handler: {@link turboInspect} with its error channel mapped onto {@link McpToolError}. */
|
|
131
77
|
const handleTurboInspect = (fallbackCwd, params) => turboInspect(params, fallbackCwd).pipe(Effect.mapError(mapEngineError(params.cwd ?? fallbackCwd, REMEDIATION)));
|
|
132
78
|
|
|
133
79
|
//#endregion
|
|
134
|
-
export { TurboAffectedResult, TurboCacheResult, TurboGraphResult,
|
|
80
|
+
export { TurboAffectedResult, TurboCacheResult, TurboGraphResult, TurboInspectParams, TurboInspectResult, handleTurboInspect, turboInspect, turboInspectTool };
|
package/tools/workspace-info.js
CHANGED
|
@@ -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 { SilkWorkspaceAnalyzer } from "@savvy-web/silk-effects";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -65,27 +64,6 @@ const toWorkspaceInfoResult = (analysis) => ({
|
|
|
65
64
|
workspaceCount: analysis.workspaces.length,
|
|
66
65
|
workspaces: analysis.workspaces.map(toSummary)
|
|
67
66
|
});
|
|
68
|
-
/** Render the structured result as a markdown transcript. */
|
|
69
|
-
const formatWorkspaceInfoMarkdown = (data) => {
|
|
70
|
-
const pm = data.packageManager.version ? `${data.packageManager.type}@${data.packageManager.version}` : data.packageManager.type;
|
|
71
|
-
const lines = [
|
|
72
|
-
`# Workspace: ${data.root}`,
|
|
73
|
-
"",
|
|
74
|
-
`- runtime: ${data.runtime}`,
|
|
75
|
-
`- package manager: ${pm}`,
|
|
76
|
-
`- workspaces: ${data.workspaceCount}`,
|
|
77
|
-
"",
|
|
78
|
-
"| name | version | publishable | versioned | tagged | released |",
|
|
79
|
-
"| --- | --- | --- | --- | --- | --- |"
|
|
80
|
-
];
|
|
81
|
-
for (const w of data.workspaces) lines.push(`| ${w.name} | ${w.version ?? "—"} | ${w.publishable} | ${w.versioned} | ${w.tagged} | ${w.released} |`);
|
|
82
|
-
return lines.join("\n");
|
|
83
|
-
};
|
|
84
|
-
/** One-way transform: result to markdown. Encoding back is forbidden. */
|
|
85
|
-
const WorkspaceInfoAsMarkdown = WorkspaceInfoResult.pipe(Schema.decodeTo(Schema.String, {
|
|
86
|
-
decode: SchemaGetter.transform(formatWorkspaceInfoMarkdown),
|
|
87
|
-
encode: SchemaGetter.forbidden(() => "WorkspaceInfoAsMarkdown is one-way: markdown cannot be parsed back.")
|
|
88
|
-
}));
|
|
89
67
|
/**
|
|
90
68
|
* Effect handler: resolve the workspace root by walking up from `base`, analyze
|
|
91
69
|
* that root, and project to the tool result. Fails with `WorkspaceRootNotFoundError`
|
|
@@ -105,12 +83,12 @@ const REMEDIATION = { hint: "The workspace could not be analyzed; check the mani
|
|
|
105
83
|
* handler record fails against `Toolkit.HandlersFrom`).
|
|
106
84
|
*/
|
|
107
85
|
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
|
|
86
|
+
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 a typed object in structuredContent (content[] carries the same object as JSON).",
|
|
109
87
|
parameters: WorkspaceInfoParams,
|
|
110
88
|
success: WorkspaceInfoResult,
|
|
111
89
|
failure: McpToolError,
|
|
112
90
|
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)
|
|
91
|
+
}).annotate(Tool.Title, "Workspace info").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true).annotate(Tool.OpenWorld, false);
|
|
114
92
|
/** Wire handler: the existing {@link workspaceInfo} program with its error channel mapped onto {@link McpToolError}. */
|
|
115
93
|
const handleWorkspaceInfo = (fallbackCwd, params) => {
|
|
116
94
|
const cwd = params.cwd ?? fallbackCwd;
|
|
@@ -118,4 +96,4 @@ const handleWorkspaceInfo = (fallbackCwd, params) => {
|
|
|
118
96
|
};
|
|
119
97
|
|
|
120
98
|
//#endregion
|
|
121
|
-
export {
|
|
99
|
+
export { WorkspaceInfoParams, WorkspaceInfoResult, WorkspaceSummary, handleWorkspaceInfo, toWorkspaceInfoResult, workspaceInfo, workspaceInfoTool };
|
package/version.js
CHANGED
package/internal/project-root.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
//#region src/internal/project-root.ts
|
|
2
|
-
/**
|
|
3
|
-
* Pure resolution of the MCP server's project working directory.
|
|
4
|
-
*
|
|
5
|
-
* @packageDocumentation
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Resolves the project directory the MCP server should root its runtime in.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Precedence: a non-empty, non-placeholder `argv[0]`, then
|
|
12
|
-
* `env.SAVVY_MCP_PROJECT_DIR`, then `env.CLAUDE_PROJECT_DIR`, then `cwd()`. A
|
|
13
|
-
* `${...}`-shaped `argv[0]` is an unresolved template placeholder (an MCP
|
|
14
|
-
* client that failed to substitute a config variable) and is ignored, as is
|
|
15
|
-
* a whitespace-only value. Takes `argv`/`env`/`cwd` as parameters rather than
|
|
16
|
-
* reading `process` directly so it stays pure and unit-testable; `main.ts` is
|
|
17
|
-
* the sole caller and supplies the real process bindings.
|
|
18
|
-
*/
|
|
19
|
-
function resolveProjectDir(argv, env, cwd) {
|
|
20
|
-
const trimmed = argv[0]?.trim();
|
|
21
|
-
return (trimmed !== void 0 && trimmed.length > 0 && !(trimmed.startsWith("${") && trimmed.endsWith("}")) ? trimmed : void 0) ?? env.SAVVY_MCP_PROJECT_DIR ?? env.CLAUDE_PROJECT_DIR ?? cwd();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
//#endregion
|
|
25
|
-
export { resolveProjectDir };
|
package/markdown.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Context } from "effect";
|
|
2
|
-
|
|
3
|
-
//#region src/markdown.ts
|
|
4
|
-
/**
|
|
5
|
-
* The tool annotation that carries each tool's markdown transcript renderer
|
|
6
|
-
* to the server's registration step.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* rc.116's `McpServer.registerToolkit` renders every success as
|
|
10
|
-
* `content: [{ type: "text", text: JSON.stringify(encodedResult) }]` plus
|
|
11
|
-
* `structuredContent` and offers no hook over the text
|
|
12
|
-
* (`unstable/ai/McpServer.ts`, `toolResultContent`; Effect-TS/effect#8316
|
|
13
|
-
* proposes one). `server.ts` therefore registers the
|
|
14
|
-
* toolkit itself through the public `McpServer.addTool` and reads this
|
|
15
|
-
* annotation to put the tool's markdown projection in `content[0].text`
|
|
16
|
-
* while `structuredContent` stays the typed object — the dual channel every
|
|
17
|
-
* tool description promises. A tool without the annotation falls back to the
|
|
18
|
-
* framework's JSON text.
|
|
19
|
-
*
|
|
20
|
-
* @packageDocumentation
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* Renders a tool's decoded success value as the markdown transcript an agent
|
|
24
|
-
* reads. Typed over `unknown` because the annotation is read generically at
|
|
25
|
-
* registration; each tool supplies `Schema.decodeUnknownSync(<X>AsMarkdown)`,
|
|
26
|
-
* whose own decoder is the type guard.
|
|
27
|
-
*
|
|
28
|
-
* @public
|
|
29
|
-
*/
|
|
30
|
-
var SilkMarkdown = class extends Context.Service()("@savvy-web/mcp/SilkMarkdown") {};
|
|
31
|
-
|
|
32
|
-
//#endregion
|
|
33
|
-
export { SilkMarkdown };
|
package/tools/md-inline.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
//#region src/tools/md-inline.ts
|
|
2
|
-
/**
|
|
3
|
-
* Render a repo-derived value as an inert markdown code span. Backslash
|
|
4
|
-
* escaping does NOT work inside code spans (CommonMark treats backslashes
|
|
5
|
-
* literally there), so this uses the delimiter-run rule instead: wrap the
|
|
6
|
-
* value in a backtick run strictly longer than any backtick run it contains,
|
|
7
|
-
* padding with spaces when the value starts or ends with a backtick. This
|
|
8
|
-
* keeps vendored-repo content (names, refs, purposes, orientation values,
|
|
9
|
-
* commit messages, note text) — the definition of untrusted input — from
|
|
10
|
-
* injecting markdown structure into the transcript an agent reads.
|
|
11
|
-
*
|
|
12
|
-
* Shared by every tool that interpolates untrusted repo content into a
|
|
13
|
-
* markdown transcript so the escaping rule has exactly one implementation.
|
|
14
|
-
*/
|
|
15
|
-
const mdInline = (value) => {
|
|
16
|
-
const runs = value.match(/`+/g) ?? [];
|
|
17
|
-
const delimiter = "`".repeat(Math.max(1, ...runs.map((run) => run.length + 1)));
|
|
18
|
-
return `${delimiter}${value.startsWith("`") || value.endsWith("`") ? ` ${value} ` : value}${delimiter}`;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
//#endregion
|
|
22
|
-
export { mdInline };
|