@vodmal/vdx-cli 0.2.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/src/init.ts ADDED
@@ -0,0 +1,309 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import {
4
+ Ctx,
5
+ fileExists,
6
+ readJson,
7
+ readText,
8
+ listAllTasks,
9
+ autoDetectStack,
10
+ } from './facts.ts';
11
+
12
+ export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
13
+ export type Verb = (typeof STANDARD_VERBS)[number];
14
+
15
+ export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v0.2.2';
16
+
17
+ const VERB_ALIASES: Record<Verb, string[]> = {
18
+ up: ['docker:up', 'docker-up', 'docker:up:detached', 'start'],
19
+ down: ['docker:down', 'docker-down', 'stop'],
20
+ build: ['docker:build', 'build:prod', 'build-prod', 'build:dev', 'build-dev', 'build:assets'],
21
+ test: ['phpunit', 'test:unit', 'test-unit', 'test:unit:phpunit', 'jest', 'vitest'],
22
+ check: ['lint', 'check:config', 'check:before:commit', 'check:code:static', 'check:code:lint'],
23
+ fix: ['fix:ecs', 'format', 'lint:fix'],
24
+ };
25
+
26
+ const VERB_PREFIX_GROUPS: Record<Verb, string[]> = {
27
+ up: [],
28
+ down: [],
29
+ build: ['build:', 'build-'],
30
+ test: ['test:', 'test-'],
31
+ check: ['check:', 'check-'],
32
+ fix: ['fix:', 'fix-'],
33
+ };
34
+
35
+ export interface VerbMapping {
36
+ verb: Verb;
37
+ nativeTask: string | null;
38
+ runCommand: string | null;
39
+ }
40
+
41
+ export interface InitPlan {
42
+ projectRoot: string;
43
+ stack: string;
44
+ pkgManager: 'composer' | 'npm' | 'pnpm' | 'yarn' | null;
45
+ tools: Record<string, string>;
46
+ mappings: VerbMapping[];
47
+ miseTomlPath: string;
48
+ miseTomlContent: string;
49
+ agentsMdPath: string;
50
+ agentsMdContent: string;
51
+ baseline: string;
52
+ }
53
+
54
+ function detectNodePackageManager(projectRoot: string): 'pnpm' | 'yarn' | 'npm' {
55
+ if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm';
56
+ if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) return 'yarn';
57
+ return 'npm';
58
+ }
59
+
60
+ function renderRunCommand(task: string, stack: string, projectRoot: string): string {
61
+ if (stack === 'php') return `composer ${task}`;
62
+ if (stack === 'node') {
63
+ const pm = detectNodePackageManager(projectRoot);
64
+ if (pm === 'yarn') return `yarn ${task}`;
65
+ return `${pm} run ${task}`;
66
+ }
67
+ return task;
68
+ }
69
+
70
+ function pickPrefixGroup(prefixes: string[], tasks: Set<string>, verb: Verb): string | null {
71
+ if (prefixes.length === 0) return null;
72
+ // Prefer entries that look like a canonical "prod/dist/release" first, then any.
73
+ const canonHints = ['prod', 'production', 'release', 'dist', 'all'];
74
+ const candidates: string[] = [];
75
+ for (const t of tasks) {
76
+ for (const pfx of prefixes) {
77
+ if (t.startsWith(pfx) && t !== verb) {
78
+ candidates.push(t);
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ if (candidates.length === 0) return null;
84
+ for (const hint of canonHints) {
85
+ const hit = candidates.find((c) => c.toLowerCase().includes(hint));
86
+ if (hit) return hit;
87
+ }
88
+ candidates.sort((a, b) => a.length - b.length);
89
+ return candidates[0] ?? null;
90
+ }
91
+
92
+ export function mapVerb(verb: Verb, tasks: Set<string>): string | null {
93
+ if (tasks.has(verb)) return verb;
94
+ for (const alt of VERB_ALIASES[verb]) {
95
+ if (tasks.has(alt)) return alt;
96
+ }
97
+ return pickPrefixGroup(VERB_PREFIX_GROUPS[verb], tasks, verb);
98
+ }
99
+
100
+ function collectTools(ctx: Ctx): Record<string, string> {
101
+ const tools: Record<string, string> = {};
102
+ const composer = readJson(ctx, 'composer.json');
103
+ if (composer?.require?.php && typeof composer.require.php === 'string') {
104
+ tools.php = stripVersionConstraint(composer.require.php);
105
+ }
106
+ const pkg = readJson(ctx, 'package.json');
107
+ if (pkg?.engines?.node && typeof pkg.engines.node === 'string') {
108
+ tools.node = stripVersionConstraint(pkg.engines.node);
109
+ }
110
+ return tools;
111
+ }
112
+
113
+ function stripVersionConstraint(raw: string): string {
114
+ const m = raw.match(/(\d+(?:\.\d+){0,2})/);
115
+ if (m && m[1]) return m[1];
116
+ return raw.trim();
117
+ }
118
+
119
+ const VERB_DESCRIPTIONS: Record<Verb, string> = {
120
+ up: 'Start the project locally',
121
+ down: 'Stop the project',
122
+ build: 'Build assets / artifacts',
123
+ test: 'Run tests',
124
+ check: 'Quality checks (read-only)',
125
+ fix: 'Auto-fixes',
126
+ };
127
+
128
+ function quoteToml(s: string): string {
129
+ // mise tasks accept basic strings; only escape backslash and double quote.
130
+ return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
131
+ }
132
+
133
+ export function renderMiseToml(
134
+ tools: Record<string, string>,
135
+ mappings: VerbMapping[],
136
+ stack: string,
137
+ baseline: string,
138
+ ): string {
139
+ const out: string[] = [];
140
+ out.push('# Generated by vdx init. The [vdx] block is metadata for vdx audit.');
141
+ out.push('# Lifecycle verbs follow the vdx contract: up/down/build/test/check/fix.');
142
+ out.push('');
143
+
144
+ if (Object.keys(tools).length > 0) {
145
+ out.push('[tools]');
146
+ for (const [k, v] of Object.entries(tools)) {
147
+ out.push(`${k} = ${quoteToml(v)}`);
148
+ }
149
+ out.push('');
150
+ }
151
+
152
+ for (const m of mappings) {
153
+ if (!m.runCommand) continue;
154
+ out.push(`[tasks.${m.verb}]`);
155
+ out.push(`description = ${quoteToml(VERB_DESCRIPTIONS[m.verb])}`);
156
+ out.push(`run = ${quoteToml(m.runCommand)}`);
157
+ out.push('');
158
+ }
159
+
160
+ out.push('[vdx]');
161
+ out.push('schema_version = "0.2"');
162
+ out.push(`baseline = ${quoteToml(baseline)}`);
163
+ out.push(`stack = ${quoteToml(stack)}`);
164
+ const verbsList = mappings
165
+ .filter((m) => m.runCommand)
166
+ .map((m) => quoteToml(m.verb))
167
+ .join(', ');
168
+ out.push(`verbs = [${verbsList}]`);
169
+ out.push('');
170
+
171
+ return out.join('\n');
172
+ }
173
+
174
+ const AGENTS_MARKER_OPEN = '<!-- vdx:commands -->';
175
+ const AGENTS_MARKER_CLOSE = '<!-- /vdx:commands -->';
176
+
177
+ export function renderAgentsCommandsSection(
178
+ mappings: VerbMapping[],
179
+ baseline: string,
180
+ ): string {
181
+ const lines: string[] = [];
182
+ lines.push(AGENTS_MARKER_OPEN);
183
+ lines.push('## Commands');
184
+ lines.push('');
185
+ lines.push(
186
+ `This project follows the vdx lifecycle interface (baseline: ${baseline}).`,
187
+ );
188
+ lines.push('');
189
+ lines.push('| Verb | What it does | Native command |');
190
+ lines.push('|------|--------------|----------------|');
191
+ for (const m of mappings) {
192
+ if (!m.runCommand) {
193
+ lines.push(`| \`mise run ${m.verb}\` | ${VERB_DESCRIPTIONS[m.verb]} | _(not mapped)_ |`);
194
+ } else {
195
+ lines.push(
196
+ `| \`mise run ${m.verb}\` | ${VERB_DESCRIPTIONS[m.verb]} | \`${m.runCommand}\` |`,
197
+ );
198
+ }
199
+ }
200
+ lines.push(AGENTS_MARKER_CLOSE);
201
+ return lines.join('\n');
202
+ }
203
+
204
+ export function buildAgentsMd(existing: string | null, section: string): string {
205
+ if (existing === null) {
206
+ return `# AGENTS\n\nProject agent guide.\n\n${section}\n`;
207
+ }
208
+ const openIdx = existing.indexOf(AGENTS_MARKER_OPEN);
209
+ const closeIdx = existing.indexOf(AGENTS_MARKER_CLOSE);
210
+ if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {
211
+ const before = existing.slice(0, openIdx);
212
+ const after = existing.slice(closeIdx + AGENTS_MARKER_CLOSE.length);
213
+ return before + section + after;
214
+ }
215
+ // No markers — append.
216
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
217
+ return existing + sep + section + '\n';
218
+ }
219
+
220
+ export function planInit(projectRoot: string, opts: { baseline?: string } = {}): InitPlan {
221
+ const stack = autoDetectStack(projectRoot);
222
+ const ctx: Ctx = { projectRoot, stack, cache: new Map() };
223
+ const tasks = listAllTasks(ctx);
224
+
225
+ const mappings: VerbMapping[] = STANDARD_VERBS.map((verb) => {
226
+ const native = mapVerb(verb, tasks);
227
+ return {
228
+ verb,
229
+ nativeTask: native,
230
+ runCommand: native ? renderRunCommand(native, stack, projectRoot) : null,
231
+ };
232
+ });
233
+
234
+ const tools = collectTools(ctx);
235
+ const baseline = opts.baseline ?? DEFAULT_BASELINE;
236
+ const miseTomlContent = renderMiseToml(tools, mappings, stack, baseline);
237
+
238
+ const agentsMdPath = path.join(projectRoot, 'AGENTS.md');
239
+ const existingAgents = fileExists(ctx, 'AGENTS.md') ? readText(ctx, 'AGENTS.md') : null;
240
+ const section = renderAgentsCommandsSection(mappings, baseline);
241
+ const agentsMdContent = buildAgentsMd(existingAgents, section);
242
+
243
+ let pkgManager: InitPlan['pkgManager'] = null;
244
+ if (stack === 'php') pkgManager = 'composer';
245
+ else if (stack === 'node') pkgManager = detectNodePackageManager(projectRoot);
246
+
247
+ return {
248
+ projectRoot,
249
+ stack,
250
+ pkgManager,
251
+ tools,
252
+ mappings,
253
+ miseTomlPath: path.join(projectRoot, 'mise.toml'),
254
+ miseTomlContent,
255
+ agentsMdPath,
256
+ agentsMdContent,
257
+ baseline,
258
+ };
259
+ }
260
+
261
+ export function writeInit(plan: InitPlan, opts: { force: boolean }): void {
262
+ if (fs.existsSync(plan.miseTomlPath) && !opts.force) {
263
+ throw new Error(
264
+ `mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`,
265
+ );
266
+ }
267
+ fs.writeFileSync(plan.miseTomlPath, plan.miseTomlContent, 'utf8');
268
+ fs.writeFileSync(plan.agentsMdPath, plan.agentsMdContent, 'utf8');
269
+ }
270
+
271
+ export function renderPlanSummary(plan: InitPlan): string {
272
+ const lines: string[] = [];
273
+ lines.push(`# vdx init plan`);
274
+ lines.push('');
275
+ lines.push(`- **Project**: ${plan.projectRoot}`);
276
+ lines.push(`- **Stack**: ${plan.stack}`);
277
+ if (plan.pkgManager) lines.push(`- **Package manager**: ${plan.pkgManager}`);
278
+ lines.push(`- **Baseline**: ${plan.baseline}`);
279
+ if (Object.keys(plan.tools).length > 0) {
280
+ const toolStr = Object.entries(plan.tools)
281
+ .map(([k, v]) => `${k}=${v}`)
282
+ .join(', ');
283
+ lines.push(`- **Tools**: ${toolStr}`);
284
+ }
285
+ lines.push('');
286
+ lines.push('## Verb mappings');
287
+ lines.push('');
288
+ lines.push('| Verb | Native task | Generated `run` |');
289
+ lines.push('|------|-------------|-----------------|');
290
+ for (const m of plan.mappings) {
291
+ if (m.runCommand) {
292
+ lines.push(`| \`${m.verb}\` | \`${m.nativeTask}\` | \`${m.runCommand}\` |`);
293
+ } else {
294
+ lines.push(`| \`${m.verb}\` | _(not found)_ | _(skipped)_ |`);
295
+ }
296
+ }
297
+ lines.push('');
298
+ const unmatched = plan.mappings.filter((m) => !m.runCommand).map((m) => m.verb);
299
+ if (unmatched.length > 0) {
300
+ lines.push(`> Not mapped: ${unmatched.join(', ')}. Add тиски в \`mise.toml\` вручную.`);
301
+ lines.push('');
302
+ }
303
+ lines.push('## Generated mise.toml');
304
+ lines.push('');
305
+ lines.push('```toml');
306
+ lines.push(plan.miseTomlContent);
307
+ lines.push('```');
308
+ return lines.join('\n') + '\n';
309
+ }
@@ -0,0 +1,52 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as TOML from 'smol-toml';
4
+ import YAML from 'js-yaml';
5
+
6
+ export interface VdxManifest {
7
+ schema_version?: string;
8
+ baseline?: string;
9
+ stack?: string;
10
+ primary_language?: string;
11
+ primary_subpackage?: string;
12
+ verbs?: string[];
13
+ shared_infra?: {
14
+ provider?: string;
15
+ precheck?: boolean;
16
+ healthcheck_url?: string;
17
+ };
18
+ success_path?: Record<string, unknown>;
19
+ audit?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface Override {
23
+ axis: string;
24
+ target?: string;
25
+ suppress?: boolean;
26
+ reason: string;
27
+ until?: string;
28
+ }
29
+
30
+ export function loadManifest(projectRoot: string): VdxManifest | null {
31
+ const miseToml = path.join(projectRoot, 'mise.toml');
32
+ if (!fs.existsSync(miseToml)) return null;
33
+ try {
34
+ const text = fs.readFileSync(miseToml, 'utf8');
35
+ const data = TOML.parse(text) as { vdx?: VdxManifest };
36
+ return data?.vdx ?? null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export function loadOverrides(projectRoot: string): Override[] {
43
+ const file = path.join(projectRoot, '.vdx-overrides.yml');
44
+ if (!fs.existsSync(file)) return [];
45
+ try {
46
+ const text = fs.readFileSync(file, 'utf8');
47
+ const data = YAML.load(text) as { overrides?: Override[] };
48
+ return data?.overrides ?? [];
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { z } from 'zod';
6
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import * as TOML from 'smol-toml';
9
+
10
+ import { loadRubric } from './rubric.ts';
11
+ import { loadManifest, loadOverrides } from './manifest.ts';
12
+ import { autoDetectStack, readToml, type Ctx } from './facts.ts';
13
+ import { audit } from './audit.ts';
14
+ import { reportJson, reportMarkdown } from './report.ts';
15
+ import { STANDARD_VERBS, type Verb } from './init.ts';
16
+ import { resolveDefaultRubric } from './defaults.ts';
17
+
18
+ const DEFAULT_RUBRIC = resolveDefaultRubric();
19
+
20
+ function resolveProjectRoot(argv: string[]): string {
21
+ const i = argv.indexOf('--project');
22
+ if (i !== -1 && argv[i + 1]) return path.resolve(argv[i + 1]!);
23
+ return process.cwd();
24
+ }
25
+
26
+ const PROJECT_ROOT = resolveProjectRoot(process.argv);
27
+
28
+ function freshCtx(): Ctx {
29
+ const stack = autoDetectStack(PROJECT_ROOT);
30
+ return { projectRoot: PROJECT_ROOT, stack, cache: new Map() };
31
+ }
32
+
33
+ function readMiseToml(): any {
34
+ return readToml(freshCtx(), 'mise.toml');
35
+ }
36
+
37
+ function getVdxBlock(): any {
38
+ const t = readMiseToml();
39
+ return t?.vdx ?? null;
40
+ }
41
+
42
+ function getTasks(): Record<string, any> {
43
+ const t = readMiseToml();
44
+ return (t?.tasks as Record<string, any>) ?? {};
45
+ }
46
+
47
+ interface RunResult {
48
+ ok: boolean;
49
+ exit_code: number;
50
+ stdout: string;
51
+ stderr: string;
52
+ duration_ms: number;
53
+ }
54
+
55
+ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
56
+ return new Promise((resolve) => {
57
+ const started = Date.now();
58
+ const child = spawn('mise', ['run', verb, ...extraArgs], {
59
+ cwd: PROJECT_ROOT,
60
+ env: process.env,
61
+ });
62
+ let stdout = '';
63
+ let stderr = '';
64
+ child.stdout.on('data', (b) => {
65
+ stdout += b.toString();
66
+ });
67
+ child.stderr.on('data', (b) => {
68
+ stderr += b.toString();
69
+ });
70
+ child.on('error', (e) => {
71
+ resolve({
72
+ ok: false,
73
+ exit_code: -1,
74
+ stdout,
75
+ stderr: stderr + `\nspawn-error: ${e.message}`,
76
+ duration_ms: Date.now() - started,
77
+ });
78
+ });
79
+ child.on('close', (code) => {
80
+ resolve({
81
+ ok: code === 0,
82
+ exit_code: code ?? -1,
83
+ stdout,
84
+ stderr,
85
+ duration_ms: Date.now() - started,
86
+ });
87
+ });
88
+ });
89
+ }
90
+
91
+ function buildCapabilities() {
92
+ const vdxBlock = getVdxBlock();
93
+ const tasks = getTasks();
94
+ const verbsList = (vdxBlock?.verbs as string[] | undefined) ?? STANDARD_VERBS.slice();
95
+ const verbs = verbsList.map((name) => {
96
+ const t = tasks[name];
97
+ const nativeCommand =
98
+ t && typeof t === 'object' && typeof t.run === 'string' ? t.run : null;
99
+ return {
100
+ name,
101
+ has_manifest: Boolean(t),
102
+ native_command: nativeCommand,
103
+ };
104
+ });
105
+
106
+ const sharedInfra = vdxBlock?.shared_infra
107
+ ? {
108
+ provider: String(vdxBlock.shared_infra.provider ?? ''),
109
+ precheck: Boolean(vdxBlock.shared_infra.precheck),
110
+ is_running: false,
111
+ }
112
+ : null;
113
+
114
+ return {
115
+ schema_version: String(vdxBlock?.schema_version ?? '0.2'),
116
+ stack: String(vdxBlock?.stack ?? autoDetectStack(PROJECT_ROOT)),
117
+ baseline: String(vdxBlock?.baseline ?? `file://${DEFAULT_RUBRIC}`),
118
+ verbs,
119
+ shared_infra: sharedInfra,
120
+ last_verified: vdxBlock?.success_path?.last_verified ?? null,
121
+ };
122
+ }
123
+
124
+ function buildAuditPayload(baselineOverride: string | undefined, fmt: 'json' | 'markdown') {
125
+ const ctx = freshCtx();
126
+ const rubricPath = DEFAULT_RUBRIC;
127
+ const rubric = loadRubric(rubricPath);
128
+ const manifest = loadManifest(PROJECT_ROOT);
129
+ const overrides = loadOverrides(PROJECT_ROOT);
130
+ const baselineRef = baselineOverride ?? manifest?.baseline ?? `file://${rubricPath}`;
131
+ const result = audit(rubric, ctx, overrides, baselineRef, manifest);
132
+ if (fmt === 'markdown') return reportMarkdown(result);
133
+ return reportJson(result);
134
+ }
135
+
136
+ function recordSuccessPath(input: {
137
+ verbs: { name: string; command: string }[];
138
+ shared_infra?: { provider: string; precheck: boolean };
139
+ notes?: string;
140
+ }): { ok: boolean; manifest_path: string; agents_md_updated: boolean } {
141
+ const manifestPath = path.join(PROJECT_ROOT, 'mise.toml');
142
+ let doc: any = {};
143
+ if (fs.existsSync(manifestPath)) {
144
+ const text = fs.readFileSync(manifestPath, 'utf8');
145
+ try {
146
+ doc = TOML.parse(text);
147
+ } catch (e: any) {
148
+ throw new Error(`mise.toml parse error: ${e?.message}`);
149
+ }
150
+ }
151
+
152
+ doc.tasks = doc.tasks ?? {};
153
+ for (const v of input.verbs) {
154
+ doc.tasks[v.name] = { run: v.command };
155
+ }
156
+
157
+ doc.vdx = doc.vdx ?? {
158
+ schema_version: '0.2',
159
+ baseline: 'github.com/VoDmAl/vdx-rubric-vodmal@v0.2.1',
160
+ stack: autoDetectStack(PROJECT_ROOT),
161
+ };
162
+ doc.vdx.verbs = input.verbs.map((v) => v.name);
163
+
164
+ if (input.shared_infra) {
165
+ doc.vdx.shared_infra = {
166
+ provider: input.shared_infra.provider,
167
+ precheck: input.shared_infra.precheck,
168
+ };
169
+ }
170
+
171
+ doc.vdx.success_path = {
172
+ last_verified: new Date().toISOString().slice(0, 10),
173
+ verified_by: 'vdx-mcp',
174
+ ...(input.notes ? { notes: input.notes } : {}),
175
+ };
176
+
177
+ const newText = TOML.stringify(doc);
178
+ fs.writeFileSync(manifestPath, newText, 'utf8');
179
+
180
+ return { ok: true, manifest_path: manifestPath, agents_md_updated: false };
181
+ }
182
+
183
+ const server = new McpServer({
184
+ name: 'vdx',
185
+ version: '0.1.0',
186
+ });
187
+
188
+ server.registerTool(
189
+ 'list_capabilities',
190
+ {
191
+ description:
192
+ 'Возвращает manifest проекта: stack, baseline, доступные глаголы и их native команды, shared_infra и last_verified. Самый частый вызов агента, входящего в проект.',
193
+ inputSchema: {},
194
+ },
195
+ async () => {
196
+ const payload = buildCapabilities();
197
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
198
+ },
199
+ );
200
+
201
+ for (const verb of STANDARD_VERBS) {
202
+ server.registerTool(
203
+ `vdx_${verb}`,
204
+ {
205
+ description: `Запускает lifecycle-глагол '${verb}' через 'mise run ${verb}'. Возвращает stdout/stderr/exit_code/duration_ms.`,
206
+ inputSchema: { args: z.array(z.string()).optional() },
207
+ },
208
+ async ({ args }) => {
209
+ const result = await spawnVerb(verb, args ?? []);
210
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
211
+ },
212
+ );
213
+ }
214
+
215
+ server.registerTool(
216
+ 'vdx_audit',
217
+ {
218
+ description:
219
+ 'Запускает аудит проекта против owner-рубрики. Возвращает JSON-отчёт по контракту (per_axis с уровнями) или markdown.',
220
+ inputSchema: {
221
+ baseline: z.string().optional(),
222
+ format: z.enum(['json', 'markdown']).optional(),
223
+ },
224
+ },
225
+ async ({ baseline, format }) => {
226
+ const fmt = format ?? 'json';
227
+ const payload = buildAuditPayload(baseline, fmt);
228
+ return { content: [{ type: 'text', text: payload }] };
229
+ },
230
+ );
231
+
232
+ server.registerTool(
233
+ 'vdx_record_success_path',
234
+ {
235
+ description:
236
+ 'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [vdx.success_path]. Используется после успешной верификации команд агентом.',
237
+ inputSchema: {
238
+ verbs: z.array(z.object({ name: z.string(), command: z.string() })),
239
+ shared_infra: z
240
+ .object({ provider: z.string(), precheck: z.boolean() })
241
+ .optional(),
242
+ notes: z.string().optional(),
243
+ },
244
+ },
245
+ async ({ verbs, shared_infra, notes }) => {
246
+ const args: Parameters<typeof recordSuccessPath>[0] = { verbs };
247
+ if (shared_infra) args.shared_infra = shared_infra;
248
+ if (notes !== undefined) args.notes = notes;
249
+ const result = recordSuccessPath(args);
250
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
251
+ },
252
+ );
253
+
254
+ async function main() {
255
+ const transport = new StdioServerTransport();
256
+ await server.connect(transport);
257
+ process.stderr.write(`vdx-mcp connected (project: ${PROJECT_ROOT})\n`);
258
+ }
259
+
260
+ main().catch((e) => {
261
+ process.stderr.write(`vdx-mcp fatal: ${e?.message ?? e}\n`);
262
+ process.exit(1);
263
+ });