@vitest-agent/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/vitest-agent-mcp.js +93 -0
- package/context.js +72 -0
- package/index.d.ts +1577 -0
- package/index.js +19 -0
- package/layers/McpLive.js +30 -0
- package/middleware/idempotency.js +128 -0
- package/package.json +58 -0
- package/prompts/explain-failure.js +27 -0
- package/prompts/index.js +89 -0
- package/prompts/regression-since-pass.js +28 -0
- package/prompts/tdd-resume.js +28 -0
- package/prompts/triage.js +24 -0
- package/prompts/why-flaky.js +30 -0
- package/prompts/wrapup.js +19 -0
- package/resources/index.js +155 -0
- package/resources/indexes.js +77 -0
- package/resources/manifest-schema.js +46 -0
- package/resources/paths.js +20 -0
- package/resources/patterns.js +22 -0
- package/resources/upstream-docs.js +22 -0
- package/router.js +74 -0
- package/server.js +838 -0
- package/tools/_tdd-error-envelope.js +98 -0
- package/tools/acceptance-metrics.js +75 -0
- package/tools/cache-health.js +83 -0
- package/tools/commit-changes.js +64 -0
- package/tools/configure.js +107 -0
- package/tools/coverage.js +76 -0
- package/tools/errors.js +151 -0
- package/tools/failure-signature-get.js +73 -0
- package/tools/file-coverage.js +106 -0
- package/tools/help.js +146 -0
- package/tools/history.js +121 -0
- package/tools/hypothesis.js +127 -0
- package/tools/inventory.js +377 -0
- package/tools/note.js +208 -0
- package/tools/overview.js +92 -0
- package/tools/ping.js +22 -0
- package/tools/register-agent.js +135 -0
- package/tools/run-tests.js +359 -0
- package/tools/settings-list.js +48 -0
- package/tools/status.js +74 -0
- package/tools/tdd-artifact.js +101 -0
- package/tools/tdd-behavior.js +177 -0
- package/tools/tdd-goal.js +147 -0
- package/tools/tdd-phase-transition-request.js +212 -0
- package/tools/tdd-task.js +278 -0
- package/tools/test.js +281 -0
- package/tools/trends.js +112 -0
- package/tools/triage-brief.js +42 -0
- package/tools/turn-search.js +60 -0
- package/tools/wrapup-prompt.js +49 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/effect-to-zod.js +81 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Match, Option, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/inventory.ts
|
|
6
|
+
/**
|
|
7
|
+
* Consolidated `inventory` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* Each `kind` produces a structured result that the boundary in
|
|
10
|
+
* server.ts can render as markdown via the exported
|
|
11
|
+
* `formatInventoryMarkdown` helper. The structured payload uses an
|
|
12
|
+
* `inventoryKind` discriminant (named separately from the input
|
|
13
|
+
* `kind` because `session` collapses to two output shapes — one for
|
|
14
|
+
* single-id lookup and one for list).
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
const ProjectRunSummary = Schema.Struct({
|
|
19
|
+
project: Schema.String,
|
|
20
|
+
lastRun: Schema.NullOr(Schema.String),
|
|
21
|
+
lastResult: Schema.NullOr(Schema.Literal("passed", "failed", "interrupted")),
|
|
22
|
+
total: Schema.Number,
|
|
23
|
+
passed: Schema.Number,
|
|
24
|
+
failed: Schema.Number,
|
|
25
|
+
skipped: Schema.Number
|
|
26
|
+
}).annotations({ identifier: "InventoryProjectRow" });
|
|
27
|
+
const ModuleRow = Schema.Struct({
|
|
28
|
+
id: Schema.Number,
|
|
29
|
+
file: Schema.String,
|
|
30
|
+
state: Schema.String,
|
|
31
|
+
testCount: Schema.Number,
|
|
32
|
+
duration: Schema.NullOr(Schema.Number)
|
|
33
|
+
}).annotations({ identifier: "InventoryModuleRow" });
|
|
34
|
+
const SuiteRow = Schema.Struct({
|
|
35
|
+
id: Schema.Number,
|
|
36
|
+
name: Schema.String,
|
|
37
|
+
module: Schema.String,
|
|
38
|
+
state: Schema.String,
|
|
39
|
+
testCount: Schema.Number
|
|
40
|
+
}).annotations({ identifier: "InventorySuiteRow" });
|
|
41
|
+
const SessionRow = Schema.Struct({
|
|
42
|
+
id: Schema.Number,
|
|
43
|
+
chatId: Schema.String,
|
|
44
|
+
project: Schema.String,
|
|
45
|
+
cwd: Schema.String,
|
|
46
|
+
agentKind: Schema.Literal("main", "subagent"),
|
|
47
|
+
agentType: Schema.NullOr(Schema.String),
|
|
48
|
+
parentSessionId: Schema.NullOr(Schema.Number),
|
|
49
|
+
triageWasNonEmpty: Schema.Boolean,
|
|
50
|
+
startedAt: Schema.String,
|
|
51
|
+
endedAt: Schema.NullOr(Schema.String),
|
|
52
|
+
endReason: Schema.NullOr(Schema.String)
|
|
53
|
+
}).annotations({ identifier: "InventorySessionRow" });
|
|
54
|
+
const ProjectInventory = Schema.Struct({
|
|
55
|
+
inventoryKind: Schema.Literal("project"),
|
|
56
|
+
count: Schema.Number,
|
|
57
|
+
projects: Schema.Array(ProjectRunSummary)
|
|
58
|
+
}).annotations({ identifier: "ProjectInventory" });
|
|
59
|
+
const ModuleGroup = Schema.Struct({
|
|
60
|
+
project: Schema.String,
|
|
61
|
+
modules: Schema.Array(ModuleRow)
|
|
62
|
+
});
|
|
63
|
+
const ModuleInventory = Schema.Struct({
|
|
64
|
+
inventoryKind: Schema.Literal("module"),
|
|
65
|
+
count: Schema.Number,
|
|
66
|
+
groups: Schema.Array(ModuleGroup)
|
|
67
|
+
}).annotations({ identifier: "ModuleInventory" });
|
|
68
|
+
const SuiteGroup = Schema.Struct({
|
|
69
|
+
project: Schema.String,
|
|
70
|
+
suites: Schema.Array(SuiteRow)
|
|
71
|
+
});
|
|
72
|
+
const SuiteInventory = Schema.Struct({
|
|
73
|
+
inventoryKind: Schema.Literal("suite"),
|
|
74
|
+
count: Schema.Number,
|
|
75
|
+
groups: Schema.Array(SuiteGroup)
|
|
76
|
+
}).annotations({ identifier: "SuiteInventory" });
|
|
77
|
+
const SessionDetailFound = Schema.Struct({
|
|
78
|
+
inventoryKind: Schema.Literal("session_detail"),
|
|
79
|
+
found: Schema.Literal(true),
|
|
80
|
+
session: SessionRow
|
|
81
|
+
}).annotations({ identifier: "SessionDetailFound" });
|
|
82
|
+
const SessionDetailMissing = Schema.Struct({
|
|
83
|
+
inventoryKind: Schema.Literal("session_detail"),
|
|
84
|
+
found: Schema.Literal(false),
|
|
85
|
+
id: Schema.Number
|
|
86
|
+
}).annotations({ identifier: "SessionDetailMissing" });
|
|
87
|
+
const SessionListInventory = Schema.Struct({
|
|
88
|
+
inventoryKind: Schema.Literal("session_list"),
|
|
89
|
+
count: Schema.Number,
|
|
90
|
+
sessions: Schema.Array(SessionRow)
|
|
91
|
+
}).annotations({ identifier: "SessionListInventory" });
|
|
92
|
+
const TagProjectBreakdown = Schema.Struct({
|
|
93
|
+
project: Schema.String,
|
|
94
|
+
moduleCount: Schema.Number,
|
|
95
|
+
testCount: Schema.Number
|
|
96
|
+
}).annotations({ identifier: "TagProjectBreakdown" });
|
|
97
|
+
const TagRowScoped = Schema.Struct({
|
|
98
|
+
tag: Schema.String,
|
|
99
|
+
moduleCount: Schema.Number,
|
|
100
|
+
testCount: Schema.Number
|
|
101
|
+
}).annotations({ identifier: "TagRowScoped" });
|
|
102
|
+
const TagRowUnscoped = Schema.Struct({
|
|
103
|
+
tag: Schema.String,
|
|
104
|
+
moduleCount: Schema.Number,
|
|
105
|
+
testCount: Schema.Number,
|
|
106
|
+
byProject: Schema.Array(TagProjectBreakdown)
|
|
107
|
+
}).annotations({ identifier: "TagRowUnscoped" });
|
|
108
|
+
const TagInventoryScoped = Schema.Struct({
|
|
109
|
+
inventoryKind: Schema.Literal("tag_scoped"),
|
|
110
|
+
project: Schema.String,
|
|
111
|
+
count: Schema.Number,
|
|
112
|
+
tags: Schema.Array(TagRowScoped)
|
|
113
|
+
}).annotations({ identifier: "TagInventoryScoped" });
|
|
114
|
+
const TagInventoryUnscoped = Schema.Struct({
|
|
115
|
+
inventoryKind: Schema.Literal("tag_unscoped"),
|
|
116
|
+
count: Schema.Number,
|
|
117
|
+
tags: Schema.Array(TagRowUnscoped)
|
|
118
|
+
}).annotations({ identifier: "TagInventoryUnscoped" });
|
|
119
|
+
const InventoryResult = Schema.Union(ProjectInventory, ModuleInventory, SuiteInventory, SessionDetailFound, SessionDetailMissing, SessionListInventory, TagInventoryScoped, TagInventoryUnscoped).annotations({
|
|
120
|
+
identifier: "InventoryResult",
|
|
121
|
+
title: "inventory result",
|
|
122
|
+
description: "Discriminate on `inventoryKind`. project/module/suite carry counted lists; session_detail discriminates further on `found`; session_list returns the matching sessions; tag_scoped and tag_unscoped carry per-tag counts (the unscoped form also carries a `byProject` breakdown per tag)."
|
|
123
|
+
});
|
|
124
|
+
const formatInventoryMarkdown = (data) => {
|
|
125
|
+
if (data.inventoryKind === "project") {
|
|
126
|
+
if (data.count === 0) return "No projects found. Run tests first.";
|
|
127
|
+
const lines = [
|
|
128
|
+
"## Projects",
|
|
129
|
+
"",
|
|
130
|
+
"| Project | Last Run | Result | Total | Passed | Failed | Skipped |",
|
|
131
|
+
"| --- | --- | --- | --- | --- | --- | --- |"
|
|
132
|
+
];
|
|
133
|
+
for (const p of data.projects) {
|
|
134
|
+
const lastRun = p.lastRun ? p.lastRun.split("T")[0] : "—";
|
|
135
|
+
const result = p.lastResult ?? "—";
|
|
136
|
+
lines.push(`| ${p.project} | ${lastRun} | ${result} | ${p.total} | ${p.passed} | ${p.failed} | ${p.skipped} |`);
|
|
137
|
+
}
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
if (data.inventoryKind === "module") {
|
|
141
|
+
if (data.count === 0) return "No modules found. Run run_tests({}) to execute tests and populate the database.";
|
|
142
|
+
const lines = ["## Modules", ""];
|
|
143
|
+
for (const g of data.groups) {
|
|
144
|
+
lines.push(`### ${g.project}`, "", "| ID | File | State | Tests | Duration |", "| --- | --- | --- | --- | --- |");
|
|
145
|
+
for (const m of g.modules) {
|
|
146
|
+
const duration = m.duration !== null ? `${m.duration}ms` : "—";
|
|
147
|
+
lines.push(`| ${m.id} | ${m.file} | ${m.state} | ${m.testCount} | ${duration} |`);
|
|
148
|
+
}
|
|
149
|
+
lines.push("");
|
|
150
|
+
}
|
|
151
|
+
return lines.join("\n").trimEnd();
|
|
152
|
+
}
|
|
153
|
+
if (data.inventoryKind === "suite") {
|
|
154
|
+
if (data.count === 0) return "No suites found. Run run_tests({}) to execute tests and populate the database.";
|
|
155
|
+
const lines = ["## Suites", ""];
|
|
156
|
+
for (const g of data.groups) {
|
|
157
|
+
lines.push(`### ${g.project}`, "", "| ID | Name | Module | State | Tests |", "| --- | --- | --- | --- | --- |");
|
|
158
|
+
for (const s of g.suites) lines.push(`| ${s.id} | ${s.name} | ${s.module} | ${s.state} | ${s.testCount} |`);
|
|
159
|
+
lines.push("");
|
|
160
|
+
}
|
|
161
|
+
return lines.join("\n").trimEnd();
|
|
162
|
+
}
|
|
163
|
+
if (data.inventoryKind === "session_detail") {
|
|
164
|
+
if (!data.found) return `No session with id=${data.id}.`;
|
|
165
|
+
const s = data.session;
|
|
166
|
+
const lines = [
|
|
167
|
+
`# Session ${s.id}`,
|
|
168
|
+
"",
|
|
169
|
+
`- chatId: \`${s.chatId}\``,
|
|
170
|
+
`- project: ${s.project}`,
|
|
171
|
+
`- agentKind: ${s.agentKind}${s.agentType !== null ? ` (${s.agentType})` : ""}`,
|
|
172
|
+
`- started: ${s.startedAt}`,
|
|
173
|
+
`- ended: ${s.endedAt ?? "still open"}`,
|
|
174
|
+
`- triageWasNonEmpty: ${s.triageWasNonEmpty}`
|
|
175
|
+
];
|
|
176
|
+
if (s.parentSessionId !== null) lines.push(`- parentSessionId: ${s.parentSessionId}`);
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
if (data.inventoryKind === "tag_scoped") {
|
|
180
|
+
if (data.count === 0) return `No tags recorded for project \`${data.project}\`. Run run_tests({}) to populate.`;
|
|
181
|
+
const lines = [
|
|
182
|
+
`## Tags — ${data.project}`,
|
|
183
|
+
"",
|
|
184
|
+
"| Tag | Modules | Tests |",
|
|
185
|
+
"| --- | --- | --- |"
|
|
186
|
+
];
|
|
187
|
+
for (const t of data.tags) lines.push(`| ${t.tag} | ${t.moduleCount} | ${t.testCount} |`);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
if (data.inventoryKind === "tag_unscoped") {
|
|
191
|
+
if (data.count === 0) return "No tags recorded. Run run_tests({}) to populate.";
|
|
192
|
+
const lines = [
|
|
193
|
+
"## Tags",
|
|
194
|
+
"",
|
|
195
|
+
"| Tag | Modules | Tests | Projects |",
|
|
196
|
+
"| --- | --- | --- | --- |"
|
|
197
|
+
];
|
|
198
|
+
for (const t of data.tags) {
|
|
199
|
+
const projectsBreakdown = t.byProject.map((p) => `${p.project} (${p.testCount})`).join(", ");
|
|
200
|
+
lines.push(`| ${t.tag} | ${t.moduleCount} | ${t.testCount} | ${projectsBreakdown} |`);
|
|
201
|
+
}
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
if (data.count === 0) return "No sessions recorded yet.";
|
|
205
|
+
const lines = ["# Sessions", ""];
|
|
206
|
+
for (const s of data.sessions) {
|
|
207
|
+
const ended = s.endedAt ? `ended ${s.endedAt}` : "open";
|
|
208
|
+
lines.push(`- **${s.chatId}** [${s.agentKind}] project=${s.project} started=${s.startedAt} ${ended}`);
|
|
209
|
+
}
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
};
|
|
212
|
+
const InventoryAsMarkdown = Schema.transformOrFail(InventoryResult, Schema.String, {
|
|
213
|
+
strict: true,
|
|
214
|
+
decode: (data) => ParseResult.succeed(formatInventoryMarkdown(data)),
|
|
215
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "InventoryAsMarkdown is one-way."))
|
|
216
|
+
});
|
|
217
|
+
const ProjectVariant = Schema.Struct({ kind: Schema.Literal("project") });
|
|
218
|
+
const ModuleVariant = Schema.Struct({
|
|
219
|
+
kind: Schema.Literal("module"),
|
|
220
|
+
project: Schema.optional(Schema.String)
|
|
221
|
+
});
|
|
222
|
+
const SuiteVariant = Schema.Struct({
|
|
223
|
+
kind: Schema.Literal("suite"),
|
|
224
|
+
project: Schema.optional(Schema.String),
|
|
225
|
+
module: Schema.optional(Schema.String)
|
|
226
|
+
});
|
|
227
|
+
const SessionVariant = Schema.Struct({
|
|
228
|
+
kind: Schema.Literal("session"),
|
|
229
|
+
id: Schema.optional(Schema.Number),
|
|
230
|
+
project: Schema.optional(Schema.String),
|
|
231
|
+
agentKind: Schema.optional(Schema.Literal("main", "subagent")),
|
|
232
|
+
limit: Schema.optional(Schema.Number)
|
|
233
|
+
});
|
|
234
|
+
const TagVariant = Schema.Struct({
|
|
235
|
+
kind: Schema.Literal("tag"),
|
|
236
|
+
project: Schema.optional(Schema.String)
|
|
237
|
+
});
|
|
238
|
+
const InventoryInput = Schema.Union(ProjectVariant, ModuleVariant, SuiteVariant, SessionVariant, TagVariant);
|
|
239
|
+
const inventory = publicProcedure.input(Schema.standardSchemaV1(InventoryInput)).query(async ({ ctx, input }) => {
|
|
240
|
+
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("kind")({
|
|
241
|
+
project: () => Effect.gen(function* () {
|
|
242
|
+
const projects = yield* (yield* DataReader).getRunsByProject();
|
|
243
|
+
return {
|
|
244
|
+
inventoryKind: "project",
|
|
245
|
+
count: projects.length,
|
|
246
|
+
projects: projects.map((p) => ({
|
|
247
|
+
project: p.project,
|
|
248
|
+
lastRun: p.lastRun,
|
|
249
|
+
lastResult: p.lastResult,
|
|
250
|
+
total: p.total,
|
|
251
|
+
passed: p.passed,
|
|
252
|
+
failed: p.failed,
|
|
253
|
+
skipped: p.skipped
|
|
254
|
+
}))
|
|
255
|
+
};
|
|
256
|
+
}),
|
|
257
|
+
module: (variant) => Effect.gen(function* () {
|
|
258
|
+
const reader = yield* DataReader;
|
|
259
|
+
const targets = variant.project ? [{ project: variant.project }] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => ({ project: r.project }))));
|
|
260
|
+
const groups = [];
|
|
261
|
+
let total = 0;
|
|
262
|
+
for (const t of targets) {
|
|
263
|
+
const modules = yield* reader.listModules(t.project);
|
|
264
|
+
if (modules.length > 0) {
|
|
265
|
+
groups.push({
|
|
266
|
+
project: t.project,
|
|
267
|
+
modules
|
|
268
|
+
});
|
|
269
|
+
total += modules.length;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
inventoryKind: "module",
|
|
274
|
+
count: total,
|
|
275
|
+
groups
|
|
276
|
+
};
|
|
277
|
+
}),
|
|
278
|
+
suite: (variant) => Effect.gen(function* () {
|
|
279
|
+
const reader = yield* DataReader;
|
|
280
|
+
const opts = {};
|
|
281
|
+
if (variant.module !== void 0) opts.module = variant.module;
|
|
282
|
+
const targets = variant.project ? [{ project: variant.project }] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => ({ project: r.project }))));
|
|
283
|
+
const groups = [];
|
|
284
|
+
let total = 0;
|
|
285
|
+
for (const t of targets) {
|
|
286
|
+
const suites = yield* reader.listSuites(t.project, opts);
|
|
287
|
+
if (suites.length > 0) {
|
|
288
|
+
groups.push({
|
|
289
|
+
project: t.project,
|
|
290
|
+
suites
|
|
291
|
+
});
|
|
292
|
+
total += suites.length;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
inventoryKind: "suite",
|
|
297
|
+
count: total,
|
|
298
|
+
groups
|
|
299
|
+
};
|
|
300
|
+
}),
|
|
301
|
+
session: (variant) => Effect.gen(function* () {
|
|
302
|
+
const reader = yield* DataReader;
|
|
303
|
+
if (variant.id !== void 0) {
|
|
304
|
+
const opt = yield* reader.getSessionById(variant.id);
|
|
305
|
+
return Option.isNone(opt) ? {
|
|
306
|
+
inventoryKind: "session_detail",
|
|
307
|
+
found: false,
|
|
308
|
+
id: variant.id
|
|
309
|
+
} : {
|
|
310
|
+
inventoryKind: "session_detail",
|
|
311
|
+
found: true,
|
|
312
|
+
session: opt.value
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const rows = yield* reader.listSessions({
|
|
316
|
+
...variant.project !== void 0 && { project: variant.project },
|
|
317
|
+
...variant.agentKind !== void 0 && { agentKind: variant.agentKind },
|
|
318
|
+
...variant.limit !== void 0 && { limit: variant.limit }
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
inventoryKind: "session_list",
|
|
322
|
+
count: rows.length,
|
|
323
|
+
sessions: rows
|
|
324
|
+
};
|
|
325
|
+
}),
|
|
326
|
+
tag: (variant) => Effect.gen(function* () {
|
|
327
|
+
const reader = yield* DataReader;
|
|
328
|
+
if (variant.project !== void 0) {
|
|
329
|
+
const rows = yield* reader.listTagInventory({ project: variant.project });
|
|
330
|
+
return {
|
|
331
|
+
inventoryKind: "tag_scoped",
|
|
332
|
+
project: variant.project,
|
|
333
|
+
count: rows.length,
|
|
334
|
+
tags: rows.map((r) => ({
|
|
335
|
+
tag: r.tag,
|
|
336
|
+
moduleCount: r.moduleCount,
|
|
337
|
+
testCount: r.testCount
|
|
338
|
+
}))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const rows = yield* reader.listTagInventory();
|
|
342
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
343
|
+
for (const r of rows) {
|
|
344
|
+
let entry = byTag.get(r.tag);
|
|
345
|
+
if (entry === void 0) {
|
|
346
|
+
entry = {
|
|
347
|
+
moduleCount: 0,
|
|
348
|
+
testCount: 0,
|
|
349
|
+
byProject: []
|
|
350
|
+
};
|
|
351
|
+
byTag.set(r.tag, entry);
|
|
352
|
+
}
|
|
353
|
+
entry.moduleCount += r.moduleCount;
|
|
354
|
+
entry.testCount += r.testCount;
|
|
355
|
+
entry.byProject.push({
|
|
356
|
+
project: r.project,
|
|
357
|
+
moduleCount: r.moduleCount,
|
|
358
|
+
testCount: r.testCount
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const tags = Array.from(byTag.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([tag, e]) => ({
|
|
362
|
+
tag,
|
|
363
|
+
moduleCount: e.moduleCount,
|
|
364
|
+
testCount: e.testCount,
|
|
365
|
+
byProject: e.byProject
|
|
366
|
+
}));
|
|
367
|
+
return {
|
|
368
|
+
inventoryKind: "tag_unscoped",
|
|
369
|
+
count: tags.length,
|
|
370
|
+
tags
|
|
371
|
+
};
|
|
372
|
+
})
|
|
373
|
+
})));
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
//#endregion
|
|
377
|
+
export { InventoryAsMarkdown, InventoryResult, inventory };
|
package/tools/note.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader, DataStore } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Match, Option, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/note.ts
|
|
6
|
+
const NoteScope = Schema.Literal("global", "project", "module", "suite", "test", "note");
|
|
7
|
+
const NoteRowSchema = Schema.Struct({
|
|
8
|
+
id: Schema.Number.annotations({ description: "Note primary key." }),
|
|
9
|
+
title: Schema.String,
|
|
10
|
+
content: Schema.String,
|
|
11
|
+
scope: NoteScope.annotations({ description: "`global` (project-agnostic), `project`, `module`, `suite`, `test` (scoped), or `note` (child note attached via `parentNoteId`)." }),
|
|
12
|
+
project: Schema.NullOr(Schema.String),
|
|
13
|
+
testFullName: Schema.NullOr(Schema.String),
|
|
14
|
+
modulePath: Schema.NullOr(Schema.String),
|
|
15
|
+
parentNoteId: Schema.NullOr(Schema.Number),
|
|
16
|
+
createdBy: Schema.NullOr(Schema.String),
|
|
17
|
+
expiresAt: Schema.NullOr(Schema.String),
|
|
18
|
+
pinned: Schema.Boolean,
|
|
19
|
+
createdAt: Schema.String,
|
|
20
|
+
updatedAt: Schema.String
|
|
21
|
+
}).annotations({ identifier: "NoteRow" });
|
|
22
|
+
const NoteCreateOk = Schema.Struct({
|
|
23
|
+
action: Schema.Literal("create"),
|
|
24
|
+
id: Schema.Number.annotations({ description: "Primary key of the newly inserted note." })
|
|
25
|
+
});
|
|
26
|
+
const NoteListOk = Schema.Struct({
|
|
27
|
+
action: Schema.Literal("list"),
|
|
28
|
+
count: Schema.Number,
|
|
29
|
+
notes: Schema.Array(NoteRowSchema).annotations({ description: "Notes matching the optional scope/project/test filters." })
|
|
30
|
+
});
|
|
31
|
+
const NoteGetFound = Schema.Struct({
|
|
32
|
+
action: Schema.Literal("get"),
|
|
33
|
+
found: Schema.Literal(true),
|
|
34
|
+
note: NoteRowSchema
|
|
35
|
+
});
|
|
36
|
+
const NoteGetMissing = Schema.Struct({
|
|
37
|
+
action: Schema.Literal("get"),
|
|
38
|
+
found: Schema.Literal(false),
|
|
39
|
+
id: Schema.Number
|
|
40
|
+
});
|
|
41
|
+
const NoteUpdateOk = Schema.Struct({
|
|
42
|
+
action: Schema.Literal("update"),
|
|
43
|
+
success: Schema.Literal(true)
|
|
44
|
+
});
|
|
45
|
+
const NoteDeleteOk = Schema.Struct({
|
|
46
|
+
action: Schema.Literal("delete"),
|
|
47
|
+
success: Schema.Literal(true)
|
|
48
|
+
});
|
|
49
|
+
const NoteSearchOk = Schema.Struct({
|
|
50
|
+
action: Schema.Literal("search"),
|
|
51
|
+
query: Schema.String,
|
|
52
|
+
count: Schema.Number,
|
|
53
|
+
notes: Schema.Array(NoteRowSchema).annotations({ description: "Notes whose title or content match the FTS5 query." })
|
|
54
|
+
});
|
|
55
|
+
const NoteResult = Schema.Union(NoteCreateOk, NoteListOk, NoteGetFound, NoteGetMissing, NoteUpdateOk, NoteDeleteOk, NoteSearchOk).annotations({
|
|
56
|
+
identifier: "NoteResult",
|
|
57
|
+
title: "note result",
|
|
58
|
+
description: "Discriminate on `action`. `get` further discriminates on `found`."
|
|
59
|
+
});
|
|
60
|
+
const renderNoteTable = (notes) => {
|
|
61
|
+
const lines = ["| ID | Title | Scope | Project | Created |", "| --- | --- | --- | --- | --- |"];
|
|
62
|
+
for (const n of notes) {
|
|
63
|
+
const proj = n.project ?? "—";
|
|
64
|
+
const created = n.createdAt.split("T")[0];
|
|
65
|
+
lines.push(`| ${n.id} | ${n.title} | ${n.scope} | ${proj} | ${created} |`);
|
|
66
|
+
}
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Markdown rendering used at the boundary for note list/search
|
|
71
|
+
* results. Mutations (create/get/update/delete) get JSON-stringify
|
|
72
|
+
* via `structuredJsonResult` instead of a markdown view.
|
|
73
|
+
*/
|
|
74
|
+
const formatNoteListMarkdown = (data) => {
|
|
75
|
+
if (data.action === "list") {
|
|
76
|
+
if (data.notes.length === 0) return "No notes found. Use note({ action: \"create\", ... }) to add notes.";
|
|
77
|
+
return [
|
|
78
|
+
"## Notes",
|
|
79
|
+
"",
|
|
80
|
+
renderNoteTable(data.notes)
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
if (data.action === "search") {
|
|
84
|
+
if (data.notes.length === 0) return "No notes matched.";
|
|
85
|
+
return [
|
|
86
|
+
`## Notes matching "${data.query}"`,
|
|
87
|
+
"",
|
|
88
|
+
renderNoteTable(data.notes)
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
return JSON.stringify(data, null, 2);
|
|
92
|
+
};
|
|
93
|
+
const CreateVariant = Schema.Struct({
|
|
94
|
+
action: Schema.Literal("create"),
|
|
95
|
+
title: Schema.String,
|
|
96
|
+
content: Schema.String,
|
|
97
|
+
scope: NoteScope,
|
|
98
|
+
project: Schema.optional(Schema.String),
|
|
99
|
+
testFullName: Schema.optional(Schema.String),
|
|
100
|
+
modulePath: Schema.optional(Schema.String),
|
|
101
|
+
parentNoteId: Schema.optional(Schema.Number),
|
|
102
|
+
createdBy: Schema.optional(Schema.String),
|
|
103
|
+
expiresAt: Schema.optional(Schema.String),
|
|
104
|
+
pinned: Schema.optional(Schema.Boolean)
|
|
105
|
+
});
|
|
106
|
+
const ListVariant = Schema.Struct({
|
|
107
|
+
action: Schema.Literal("list"),
|
|
108
|
+
scope: Schema.optional(Schema.String),
|
|
109
|
+
project: Schema.optional(Schema.String),
|
|
110
|
+
testFullName: Schema.optional(Schema.String)
|
|
111
|
+
});
|
|
112
|
+
const GetVariant = Schema.Struct({
|
|
113
|
+
action: Schema.Literal("get"),
|
|
114
|
+
id: Schema.Number
|
|
115
|
+
});
|
|
116
|
+
const UpdateVariant = Schema.Struct({
|
|
117
|
+
action: Schema.Literal("update"),
|
|
118
|
+
id: Schema.Number,
|
|
119
|
+
title: Schema.optional(Schema.String),
|
|
120
|
+
content: Schema.optional(Schema.String),
|
|
121
|
+
pinned: Schema.optional(Schema.Boolean),
|
|
122
|
+
expiresAt: Schema.optional(Schema.String)
|
|
123
|
+
});
|
|
124
|
+
const DeleteVariant = Schema.Struct({
|
|
125
|
+
action: Schema.Literal("delete"),
|
|
126
|
+
id: Schema.Number
|
|
127
|
+
});
|
|
128
|
+
const SearchVariant = Schema.Struct({
|
|
129
|
+
action: Schema.Literal("search"),
|
|
130
|
+
query: Schema.String
|
|
131
|
+
});
|
|
132
|
+
const NoteInputUnion = Schema.Union(CreateVariant, ListVariant, GetVariant, UpdateVariant, DeleteVariant, SearchVariant);
|
|
133
|
+
const note = publicProcedure.input(Schema.standardSchemaV1(NoteInputUnion)).mutation(async ({ ctx, input }) => {
|
|
134
|
+
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
135
|
+
create: (variant) => Effect.gen(function* () {
|
|
136
|
+
const store = yield* DataStore;
|
|
137
|
+
const noteInput = {
|
|
138
|
+
title: variant.title,
|
|
139
|
+
content: variant.content,
|
|
140
|
+
scope: variant.scope,
|
|
141
|
+
...variant.project !== void 0 && { project: variant.project },
|
|
142
|
+
...variant.testFullName !== void 0 && { testFullName: variant.testFullName },
|
|
143
|
+
...variant.modulePath !== void 0 && { modulePath: variant.modulePath },
|
|
144
|
+
...variant.parentNoteId !== void 0 && { parentNoteId: variant.parentNoteId },
|
|
145
|
+
...variant.createdBy !== void 0 && { createdBy: variant.createdBy },
|
|
146
|
+
...variant.expiresAt !== void 0 && { expiresAt: variant.expiresAt },
|
|
147
|
+
...variant.pinned !== void 0 && { pinned: variant.pinned }
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
action: "create",
|
|
151
|
+
id: yield* store.writeNote(noteInput)
|
|
152
|
+
};
|
|
153
|
+
}),
|
|
154
|
+
list: (variant) => Effect.gen(function* () {
|
|
155
|
+
const notes = yield* (yield* DataReader).getNotes(variant.scope, variant.project, variant.testFullName);
|
|
156
|
+
return {
|
|
157
|
+
action: "list",
|
|
158
|
+
count: notes.length,
|
|
159
|
+
notes
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
get: (variant) => Effect.gen(function* () {
|
|
163
|
+
const noteOpt = yield* (yield* DataReader).getNoteById(variant.id);
|
|
164
|
+
return Option.isNone(noteOpt) ? {
|
|
165
|
+
action: "get",
|
|
166
|
+
found: false,
|
|
167
|
+
id: variant.id
|
|
168
|
+
} : {
|
|
169
|
+
action: "get",
|
|
170
|
+
found: true,
|
|
171
|
+
note: noteOpt.value
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
update: (variant) => Effect.gen(function* () {
|
|
175
|
+
const store = yield* DataStore;
|
|
176
|
+
const fields = {
|
|
177
|
+
...variant.title !== void 0 && { title: variant.title },
|
|
178
|
+
...variant.content !== void 0 && { content: variant.content },
|
|
179
|
+
...variant.pinned !== void 0 && { pinned: variant.pinned },
|
|
180
|
+
...variant.expiresAt !== void 0 && { expiresAt: variant.expiresAt }
|
|
181
|
+
};
|
|
182
|
+
yield* store.updateNote(variant.id, fields);
|
|
183
|
+
return {
|
|
184
|
+
action: "update",
|
|
185
|
+
success: true
|
|
186
|
+
};
|
|
187
|
+
}),
|
|
188
|
+
delete: (variant) => Effect.gen(function* () {
|
|
189
|
+
yield* (yield* DataStore).deleteNote(variant.id);
|
|
190
|
+
return {
|
|
191
|
+
action: "delete",
|
|
192
|
+
success: true
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
195
|
+
search: (variant) => Effect.gen(function* () {
|
|
196
|
+
const notes = yield* (yield* DataReader).searchNotes(variant.query);
|
|
197
|
+
return {
|
|
198
|
+
action: "search",
|
|
199
|
+
query: variant.query,
|
|
200
|
+
count: notes.length,
|
|
201
|
+
notes
|
|
202
|
+
};
|
|
203
|
+
})
|
|
204
|
+
})));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
export { NoteResult, formatNoteListMarkdown, note };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Option, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/overview.ts
|
|
6
|
+
/**
|
|
7
|
+
* `test_overview` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
const ProjectRunSummary = Schema.Struct({
|
|
12
|
+
project: Schema.String,
|
|
13
|
+
lastRun: Schema.NullOr(Schema.String),
|
|
14
|
+
lastResult: Schema.NullOr(Schema.Literal("passed", "failed", "interrupted")),
|
|
15
|
+
total: Schema.Number,
|
|
16
|
+
passed: Schema.Number,
|
|
17
|
+
failed: Schema.Number,
|
|
18
|
+
skipped: Schema.Number
|
|
19
|
+
}).annotations({
|
|
20
|
+
identifier: "ProjectRunSummary",
|
|
21
|
+
description: "One row per project's most recent run summary."
|
|
22
|
+
});
|
|
23
|
+
const OverviewAvailable = Schema.Struct({
|
|
24
|
+
dataAvailable: Schema.Literal(true),
|
|
25
|
+
projectFilter: Schema.optional(Schema.String),
|
|
26
|
+
runs: Schema.Array(ProjectRunSummary)
|
|
27
|
+
}).annotations({ identifier: "TestOverviewAvailable" });
|
|
28
|
+
const OverviewAbsent = Schema.Struct({
|
|
29
|
+
dataAvailable: Schema.Literal(false),
|
|
30
|
+
projectFilter: Schema.optional(Schema.String),
|
|
31
|
+
reason: Schema.Literal("no_runs", "project_filter_empty")
|
|
32
|
+
}).annotations({ identifier: "TestOverviewAbsent" });
|
|
33
|
+
const TestOverviewResult = Schema.Union(OverviewAvailable, OverviewAbsent).annotations({
|
|
34
|
+
identifier: "TestOverviewResult",
|
|
35
|
+
title: "test_overview result",
|
|
36
|
+
description: "Per-project run metrics. Discriminate on `dataAvailable` for cold-start handling."
|
|
37
|
+
});
|
|
38
|
+
const iconForResult = (r) => {
|
|
39
|
+
if (r === "passed") return "✅";
|
|
40
|
+
if (r === "failed") return "❌";
|
|
41
|
+
if (r === "interrupted") return "⚠️";
|
|
42
|
+
return "⬜";
|
|
43
|
+
};
|
|
44
|
+
const formatTestOverviewMarkdown = (data) => {
|
|
45
|
+
if (!data.dataAvailable) {
|
|
46
|
+
if (data.reason === "project_filter_empty") return `No test data found for project \`${data.projectFilter ?? "(unknown)"}\`. Run tests first.`;
|
|
47
|
+
return "No test data available. Run tests first.";
|
|
48
|
+
}
|
|
49
|
+
const lines = ["# Test Overview", ""];
|
|
50
|
+
const projectGroups = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const run of data.runs) {
|
|
52
|
+
const group = projectGroups.get(run.project) ?? [];
|
|
53
|
+
group.push(run);
|
|
54
|
+
projectGroups.set(run.project, group);
|
|
55
|
+
}
|
|
56
|
+
for (const [projectName, projectRuns] of projectGroups) {
|
|
57
|
+
lines.push(`## ${projectName}`, "");
|
|
58
|
+
for (const run of projectRuns) {
|
|
59
|
+
const lastRun = run.lastRun ? new Date(run.lastRun).toLocaleString() : "never";
|
|
60
|
+
lines.push(`### ${iconForResult(run.lastResult)} ${run.project}`, "", "| Metric | Count |", "| --- | --- |", `| Total | ${run.total} |`, `| Passed | ${run.passed} |`, `| Failed | ${run.failed} |`, `| Skipped | ${run.skipped} |`, `| Last run | ${lastRun} |`, "");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
};
|
|
65
|
+
const TestOverviewAsMarkdown = Schema.transformOrFail(TestOverviewResult, Schema.String, {
|
|
66
|
+
strict: true,
|
|
67
|
+
decode: (data) => ParseResult.succeed(formatTestOverviewMarkdown(data)),
|
|
68
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TestOverviewAsMarkdown is one-way."))
|
|
69
|
+
});
|
|
70
|
+
const testOverview = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({ project: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
71
|
+
const reader = yield* DataReader;
|
|
72
|
+
const [manifestOpt, runs] = yield* Effect.all([reader.getManifest(), reader.getRunsByProject()]);
|
|
73
|
+
if (Option.isNone(manifestOpt) || runs.length === 0) return {
|
|
74
|
+
dataAvailable: false,
|
|
75
|
+
reason: "no_runs",
|
|
76
|
+
...input.project !== void 0 && { projectFilter: input.project }
|
|
77
|
+
};
|
|
78
|
+
const filteredRuns = input.project === void 0 ? runs : runs.filter((r) => r.project === input.project);
|
|
79
|
+
if (filteredRuns.length === 0) return {
|
|
80
|
+
dataAvailable: false,
|
|
81
|
+
reason: "project_filter_empty",
|
|
82
|
+
...input.project !== void 0 && { projectFilter: input.project }
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
dataAvailable: true,
|
|
86
|
+
...input.project !== void 0 && { projectFilter: input.project },
|
|
87
|
+
runs: filteredRuns
|
|
88
|
+
};
|
|
89
|
+
})));
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { TestOverviewAsMarkdown, TestOverviewResult, testOverview };
|
package/tools/ping.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/tools/ping.ts
|
|
5
|
+
/**
|
|
6
|
+
* `ping` MCP tool — Schema-driven implementation.
|
|
7
|
+
*
|
|
8
|
+
* Trivial liveness probe used to verify hot-patch reload of the MCP
|
|
9
|
+
* server. Returns the canonical `pong` payload so callers can assert
|
|
10
|
+
* a healthy round-trip.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
const PingResult = Schema.Struct({ message: Schema.Literal("pong").annotations({ description: "Constant `pong`. Presence confirms the MCP server responded." }) }).annotations({
|
|
15
|
+
identifier: "PingResult",
|
|
16
|
+
title: "ping result",
|
|
17
|
+
description: "Liveness probe. Carries no data beyond the constant `pong` discriminant."
|
|
18
|
+
});
|
|
19
|
+
const ping = publicProcedure.query(async () => ({ message: "pong" }));
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { PingResult, ping };
|