@vodmal/vdx-cli 0.24.0 → 0.26.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/index.ts CHANGED
@@ -2,14 +2,10 @@
2
2
  import * as path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { loadRubric } from './rubric.ts';
5
- import { loadManifest, loadOverrides } from './manifest.ts';
5
+ import { loadManifest, loadManifestAt, loadOverrides } from './manifest.ts';
6
6
  import { autoDetectStack, type Ctx } from './facts.ts';
7
- import {
8
- LIFECYCLE_VERBS,
9
- type LifecycleVerb,
10
- resolveLifecycleVerb,
11
- renderResolveError,
12
- } from './run.ts';
7
+ import { at, errorMessage } from './narrow.ts';
8
+ import { LIFECYCLE_VERBS, type LifecycleVerb, resolveLifecycleVerb, renderResolveError } from './run.ts';
13
9
  import { audit } from './audit.ts';
14
10
  import {
15
11
  reportMarkdown,
@@ -19,7 +15,14 @@ import {
19
15
  reportDoctorJson,
20
16
  reportDoctorAnsi,
21
17
  } from './report.ts';
22
- import { runDoctor, runDoctorCheck, runDoctorFix, resolveDoctorCtx, looksLikeProject, readCliVersion } from './doctor.ts';
18
+ import {
19
+ runDoctor,
20
+ runDoctorCheck,
21
+ runDoctorFix,
22
+ resolveDoctorCtx,
23
+ looksLikeProject,
24
+ readCliVersion,
25
+ } from './doctor.ts';
23
26
  import { planInit, writeInit, renderPlanSummary } from './init.ts';
24
27
  import {
25
28
  planPublish,
@@ -66,6 +69,7 @@ interface CommandSpec {
66
69
  }
67
70
 
68
71
  const AI_HELP = `Starts your agent in the project, or attaches to the one already running there.
72
+ Several running there: a list to pick from — Enter takes the newest.
69
73
  Which agent, its flags and whether it runs in tmux come from your profile:
70
74
  $VDX_ENVIRONMENT, else ~/.vdx-environment.yaml, else a plain claude/codex.
71
75
 
@@ -75,15 +79,20 @@ the project's files). Hand a person \`vdx ai\`, not \`<agent> --<flag>\`.
75
79
 
76
80
  project_path the project (default: cwd); its git toplevel is used
77
81
  @host the same command on <host> over ssh: its vdx, profile and tmux
78
- --new a new conversation instead of continuing the last one
79
- --focused a session nothing wakes: the agent, its hooks and MCP servers get
80
- VDX_FOCUSED=1 — for the plugins that wake sessions to leave it
81
- alone — and the profile's rules marked \`wakes: true\` (a channel
82
- flag) are left out. A running agent keeps its mode; --restart
83
- applies this one
82
+ --new a new conversation instead of continuing the last one; an agent
83
+ already running here keeps running, the new one starts next to
84
+ it (a new tmux window)
85
+ --focused a new conversation, as --new, in a session nothing wakes: the
86
+ agent, its hooks and MCP servers get VDX_FOCUSED=1 — for the
87
+ plugins that wake sessions to leave it alone — and the profile's
88
+ rules marked \`wakes: true\` (a channel flag) are left out. Back
89
+ to it: --focused --conversation <id>. A running agent keeps its
90
+ mode: plain vdx ai attaches to it as it is
84
91
  --conversation <id>
85
- continue this Claude Code conversation
86
- --restart restart the running agent in its pane — applies a changed profile.
92
+ continue this Claude Code conversation: attach to the agent in
93
+ it, or start one next to the agents running here
94
+ --restart restart the running agent in its pane — applies a changed profile;
95
+ with --new, --focused or --conversation, it is replaced by that.
87
96
  Without it, an agent running without the profile's flags is
88
97
  a question in a terminal: Enter attaches to it as is, r
89
98
  restarts it, q leaves it
@@ -251,7 +260,9 @@ function checkArgs(name: string, opts: ParsedArgs): ParsedArgs {
251
260
  process.exit(2);
252
261
  };
253
262
  const unknown = [
254
- ...Object.keys(flags).filter((f) => !bools.includes(f) && !values.includes(f)).map((f) => `--${f}`),
263
+ ...Object.keys(flags)
264
+ .filter((f) => !bools.includes(f) && !values.includes(f))
265
+ .map((f) => `--${f}`),
255
266
  ...positionals.filter((p) => p.startsWith('-')),
256
267
  ];
257
268
  if (unknown.length > 0) fail(`unknown option ${unknown.join(', ')}`);
@@ -275,14 +286,20 @@ function cmdAudit(opts: ParsedArgs): void {
275
286
  );
276
287
  }
277
288
 
278
- const manifest = loadManifest(projectRoot);
289
+ const found = loadManifestAt(projectRoot);
290
+ const manifest = found?.manifest ?? null;
291
+ if (found?.place === 'vdx') {
292
+ process.stderr.write(
293
+ 'note: the manifest is in [vdx] of mise.toml, and mise 2026.10 warns about it on every run; ' +
294
+ 'its place is [_.vdx], once every machine of the project runs vdx 0.26 or newer\n',
295
+ );
296
+ }
279
297
  const overrides = loadOverrides(projectRoot);
280
298
 
281
299
  const rubricPath = (opts.flags.rubric as string) || DEFAULT_RUBRIC;
282
300
  const rubric = loadRubric(rubricPath);
283
301
 
284
- const stack =
285
- (opts.flags.stack as string) || manifest?.stack || autoDetectStack(projectRoot);
302
+ const stack = (opts.flags.stack as string) || manifest?.stack || autoDetectStack(projectRoot);
286
303
 
287
304
  const ctx: Ctx = {
288
305
  projectRoot,
@@ -293,8 +310,7 @@ function cmdAudit(opts: ParsedArgs): void {
293
310
  const baselineRef = manifest?.baseline ?? `file://${rubricPath}`;
294
311
  const result = audit(rubric, ctx, overrides, baselineRef, manifest);
295
312
 
296
- const formatFlag =
297
- typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
313
+ const formatFlag = typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
298
314
  const wantJson = opts.flags.json === true || formatFlag === 'json';
299
315
  const wantMarkdown = formatFlag === 'markdown' || formatFlag === 'md';
300
316
  const wantAnsi = formatFlag === 'ansi';
@@ -338,8 +354,8 @@ function cmdInit(opts: ParsedArgs): void {
338
354
  try {
339
355
  writeInit(plan, { force: Boolean(opts.flags.force) });
340
356
  process.stderr.write(`\n✓ wrote ${plan.miseTomlPath}\n`);
341
- } catch (e: any) {
342
- process.stderr.write(`\nerror: ${e?.message ?? String(e)}\n`);
357
+ } catch (e) {
358
+ process.stderr.write(`\nerror: ${errorMessage(e)}\n`);
343
359
  process.exit(2);
344
360
  }
345
361
  }
@@ -347,9 +363,7 @@ function cmdInit(opts: ParsedArgs): void {
347
363
  function cmdPublish(opts: ParsedArgs): void {
348
364
  const bumpArg = opts.positionals[0];
349
365
  if (!bumpArg || !['patch', 'minor', 'major'].includes(bumpArg)) {
350
- process.stderr.write(
351
- 'Usage: vdx publish <patch|minor|major> [--dry-run] [--force] [--no-push]\n',
352
- );
366
+ process.stderr.write('Usage: vdx publish <patch|minor|major> [--dry-run] [--force] [--no-push]\n');
353
367
  process.exit(1);
354
368
  }
355
369
  const bump = bumpArg as BumpKind;
@@ -374,8 +388,8 @@ function cmdPublish(opts: ParsedArgs): void {
374
388
  const replan = () => {
375
389
  try {
376
390
  return planPublish(planOpts, auditResult, ctx);
377
- } catch (e: any) {
378
- process.stderr.write(`error: ${e?.message ?? String(e)}\n`);
391
+ } catch (e) {
392
+ process.stderr.write(`error: ${errorMessage(e)}\n`);
379
393
  process.exit(2);
380
394
  }
381
395
  };
@@ -412,8 +426,8 @@ function cmdPublish(opts: ParsedArgs): void {
412
426
 
413
427
  try {
414
428
  executePublish(plan, deps);
415
- } catch (e: any) {
416
- process.stderr.write(`\nerror: ${e?.message ?? String(e)}\n`);
429
+ } catch (e) {
430
+ process.stderr.write(`\nerror: ${errorMessage(e)}\n`);
417
431
  process.exit(3);
418
432
  }
419
433
  }
@@ -428,15 +442,15 @@ function cmdRun(verb: LifecycleVerb): void {
428
442
 
429
443
  try {
430
444
  execFileSync('mise', ['run', verb], { cwd: projectRoot, stdio: 'inherit' });
431
- } catch (e: any) {
432
- if (e?.code === 'ENOENT') {
445
+ } catch (e) {
446
+ if (at(e, 'code') === 'ENOENT') {
433
447
  process.stderr.write(
434
- `vdx: \`mise\` binary not found on PATH\n` +
435
- `hint: install mise — https://mise.jdx.dev/getting-started.html\n`,
448
+ `vdx: \`mise\` binary not found on PATH\n` + `hint: install mise — https://mise.jdx.dev/getting-started.html\n`,
436
449
  );
437
450
  process.exit(127);
438
451
  }
439
- process.exit(typeof e?.status === 'number' ? e.status : 1);
452
+ const status = at(e, 'status');
453
+ process.exit(typeof status === 'number' ? status : 1);
440
454
  }
441
455
  }
442
456
 
@@ -454,16 +468,17 @@ function cmdDoctor(opts: ParsedArgs): void {
454
468
  if (opts.flags.fix === true) {
455
469
  try {
456
470
  const done = runDoctorFix();
457
- process.stderr.write(done.length > 0 ? done.map((d) => `✓ ${d}`).join('\n') + '\n' : 'nothing to fix in ~/.gitconfig\n');
458
- } catch (e: any) {
459
- process.stderr.write(`vdx doctor --fix: ${e?.message ?? String(e)}\n`);
471
+ process.stderr.write(
472
+ done.length > 0 ? done.map((d) => `✓ ${d}`).join('\n') + '\n' : 'nothing to fix in ~/.gitconfig\n',
473
+ );
474
+ } catch (e) {
475
+ process.stderr.write(`vdx doctor --fix: ${errorMessage(e)}\n`);
460
476
  process.exit(2);
461
477
  }
462
478
  }
463
479
  const report = runDoctor(ctx);
464
480
 
465
- const formatFlag =
466
- typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
481
+ const formatFlag = typeof opts.flags.format === 'string' ? opts.flags.format : undefined;
467
482
  const wantJson = opts.flags.json === true || formatFlag === 'json';
468
483
  const wantMarkdown = formatFlag === 'markdown' || formatFlag === 'md';
469
484
  const wantAnsi = formatFlag === 'ansi';
package/src/init.ts CHANGED
@@ -1,32 +1,17 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import {
4
- Ctx,
5
- readJson,
6
- listAllTasks,
7
- autoDetectStack,
8
- findSubPackages,
9
- type SubPackage,
10
- } from './facts.ts';
3
+ import { Ctx, readJson, listAllTasks, autoDetectStack, findSubPackages, type SubPackage } from './facts.ts';
4
+ import { at } from './narrow.ts';
11
5
 
12
6
  export const STANDARD_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
13
7
  export type Verb = (typeof STANDARD_VERBS)[number];
14
8
 
15
- export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.3.0';
9
+ export const DEFAULT_BASELINE = 'github.com/VoDmAl/vdx-rubric-vodmal@v1.4.2';
16
10
 
17
11
  const VERB_ALIASES: Record<Verb, string[]> = {
18
12
  up: ['docker:up', 'docker-up', 'docker:up:detached', 'start', 'dev', 'serve'],
19
13
  down: ['docker:down', 'docker-down', 'stop'],
20
- build: [
21
- 'docker:build',
22
- 'build:prod',
23
- 'build-prod',
24
- 'build:dev',
25
- 'build-dev',
26
- 'build:assets',
27
- 'compile',
28
- 'dist',
29
- ],
14
+ build: ['docker:build', 'build:prod', 'build-prod', 'build:dev', 'build-dev', 'build:assets', 'compile', 'dist'],
30
15
  test: [
31
16
  'phpunit',
32
17
  'pest',
@@ -143,12 +128,7 @@ function renderRunCommand(task: string, stack: string, projectRoot: string): str
143
128
  return task;
144
129
  }
145
130
 
146
- function renderRunCommandInSubpackage(
147
- task: string,
148
- subStack: string,
149
- subAbs: string,
150
- subRel: string,
151
- ): string {
131
+ function renderRunCommandInSubpackage(task: string, subStack: string, subAbs: string, subRel: string): string {
152
132
  return `cd ${subRel} && ${renderRunCommand(task, subStack, subAbs)}`;
153
133
  }
154
134
 
@@ -246,14 +226,10 @@ export function mapVerb(verb: Verb, tasks: Set<string>): string | null {
246
226
 
247
227
  function collectTools(ctx: Ctx): Record<string, string> {
248
228
  const tools: Record<string, string> = {};
249
- const composer = readJson(ctx, 'composer.json');
250
- if (composer?.require?.php && typeof composer.require.php === 'string') {
251
- tools.php = stripVersionConstraint(composer.require.php);
252
- }
253
- const pkg = readJson(ctx, 'package.json');
254
- if (pkg?.engines?.node && typeof pkg.engines.node === 'string') {
255
- tools.node = stripVersionConstraint(pkg.engines.node);
256
- }
229
+ const php = at(readJson(ctx, 'composer.json'), 'require', 'php');
230
+ if (typeof php === 'string' && php) tools.php = stripVersionConstraint(php);
231
+ const node = at(readJson(ctx, 'package.json'), 'engines', 'node');
232
+ if (typeof node === 'string' && node) tools.node = stripVersionConstraint(node);
257
233
  return tools;
258
234
  }
259
235
 
@@ -285,16 +261,12 @@ export function renderMiseToml(
285
261
  primarySubpackage: string | null = null,
286
262
  ): string {
287
263
  const out: string[] = [];
288
- out.push('# Generated by vdx init. The [vdx] block is metadata for vdx audit.');
264
+ out.push('# Generated by vdx init. The [_.vdx] block is metadata for vdx audit; mise leaves [_] alone.');
289
265
  out.push('# Lifecycle verbs follow the vdx contract: up/down/build/test/check/fix.');
290
266
  out.push('');
291
267
 
292
268
  if (stack === 'unknown' || stack === 'monorepo') {
293
- out.push(
294
- '# TODO(vdx): stack auto-detection вернул "' +
295
- stack +
296
- '". Замени на php/node/go/python/meta',
297
- );
269
+ out.push('# TODO(vdx): stack auto-detection вернул "' + stack + '". Замени на php/node/go/python/meta');
298
270
  out.push('# и заполни [tasks.*] блоки руками, либо перегенери: vdx init --stack <id>.');
299
271
  out.push('');
300
272
  }
@@ -319,7 +291,7 @@ export function renderMiseToml(
319
291
  out.push('');
320
292
  }
321
293
 
322
- out.push('[vdx]');
294
+ out.push('[_.vdx]');
323
295
  out.push('schema_version = "0.2"');
324
296
  out.push(`baseline = ${quoteToml(baseline)}`);
325
297
  out.push(`stack = ${quoteToml(stack)}`);
@@ -336,10 +308,7 @@ export function renderMiseToml(
336
308
  return out.join('\n');
337
309
  }
338
310
 
339
- export function planInit(
340
- projectRoot: string,
341
- opts: { baseline?: string; stack?: string } = {},
342
- ): InitPlan {
311
+ export function planInit(projectRoot: string, opts: { baseline?: string; stack?: string } = {}): InitPlan {
343
312
  const detectedStack = autoDetectStack(projectRoot);
344
313
  const stackOverridden = opts.stack !== undefined && opts.stack !== detectedStack;
345
314
  const stack = opts.stack ?? detectedStack;
@@ -365,7 +334,7 @@ export function planInit(
365
334
  const list = subs.map((s) => `${s.relPath}(${s.stack})`).join(', ');
366
335
  warnings.push(
367
336
  `stack="meta", найдено несколько subpackages: ${list}. ` +
368
- 'primary_subpackage не выбран; укажи его руками в [vdx] блоке.',
337
+ 'primary_subpackage не выбран; укажи его руками в блоке [_.vdx].',
369
338
  );
370
339
  }
371
340
  } else if (!stackOverridden && (detectedStack === 'unknown' || detectedStack === 'monorepo')) {
@@ -436,9 +405,7 @@ export function planInit(
436
405
 
437
406
  export function writeInit(plan: InitPlan, opts: { force: boolean }): void {
438
407
  if (fs.existsSync(plan.miseTomlPath) && !opts.force) {
439
- throw new Error(
440
- `mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`,
441
- );
408
+ throw new Error(`mise.toml уже существует: ${plan.miseTomlPath}. Используй --force для перезаписи.`);
442
409
  }
443
410
  fs.writeFileSync(plan.miseTomlPath, plan.miseTomlContent, 'utf8');
444
411
  }
@@ -451,7 +418,9 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
451
418
 
452
419
  if (!verbose) {
453
420
  const lines: string[] = [];
454
- lines.push(`vdx init: stack=${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}, ${mapped}/${total} verbs mapped`);
421
+ lines.push(
422
+ `vdx init: stack=${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}, ${mapped}/${total} verbs mapped`,
423
+ );
455
424
  if (plan.primarySubpackage) lines.push(` primary subpackage: ${plan.primarySubpackage}`);
456
425
  if (unmatched.length > 0) {
457
426
  lines.push(` not mapped: ${unmatched.join(', ')} (add [tasks.X] in mise.toml manually)`);
@@ -462,12 +431,8 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
462
431
  const lines: string[] = [];
463
432
  lines.push(`# vdx init plan`);
464
433
  lines.push('');
465
- lines.push(
466
- `- **Project**: ${plan.projectRoot}`,
467
- );
468
- lines.push(
469
- `- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`,
470
- );
434
+ lines.push(`- **Project**: ${plan.projectRoot}`);
435
+ lines.push(`- **Stack**: ${plan.stack}${plan.stackOverridden ? ' (via --stack)' : ''}`);
471
436
  if (plan.primarySubpackage) {
472
437
  lines.push(`- **Primary subpackage**: ${plan.primarySubpackage}`);
473
438
  }
@@ -492,9 +457,7 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
492
457
  lines.push('|------|-------------|--------|-----------------|');
493
458
  for (const m of plan.mappings) {
494
459
  if (m.runCommand) {
495
- lines.push(
496
- `| \`${m.verb}\` | \`${m.nativeTask}\` | ${m.reason} | \`${m.runCommand}\` |`,
497
- );
460
+ lines.push(`| \`${m.verb}\` | \`${m.nativeTask}\` | ${m.reason} | \`${m.runCommand}\` |`);
498
461
  } else {
499
462
  lines.push(`| \`${m.verb}\` | _(not found)_ | not-found | _(skipped)_ |`);
500
463
  }
@@ -506,9 +469,7 @@ export function renderPlanSummary(plan: InitPlan, opts: { verbose?: boolean } =
506
469
  lines.push('');
507
470
  for (const m of withAlts) {
508
471
  const alts = m.alternatives.map((a) => `\`${a}\``).join(', ');
509
- lines.push(
510
- `- \`${m.verb}\` chose \`${m.nativeTask}\` (${m.reason}); also matched: ${alts}`,
511
- );
472
+ lines.push(`- \`${m.verb}\` chose \`${m.nativeTask}\` (${m.reason}); also matched: ${alts}`);
512
473
  }
513
474
  lines.push('');
514
475
  }
package/src/manifest.ts CHANGED
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as TOML from 'smol-toml';
4
4
  import YAML from 'js-yaml';
5
+ import { at, dict, type Dict } from './narrow.ts';
5
6
 
6
7
  export interface VdxManifest {
7
8
  schema_version?: string;
@@ -29,18 +30,38 @@ export interface Override {
29
30
  until?: string;
30
31
  }
31
32
 
32
- export function loadManifest(projectRoot: string): VdxManifest | null {
33
+ /**
34
+ * Where the manifest lives in mise.toml. `[_.vdx]` — mise documents the `[_]` table as never
35
+ * read, for other tools' data. `[vdx]` — where it lived before vdx 0.26; still read, but mise
36
+ * 2026.10 warns about an unknown field on every run.
37
+ */
38
+ export type ManifestPlace = '_.vdx' | 'vdx';
39
+
40
+ /** The manifest table of a parsed mise.toml and its place: `[_.vdx]`, else the older `[vdx]`. */
41
+ export function manifestTable(data: unknown): { table: Dict; place: ManifestPlace } | null {
42
+ const table = dict(at(data, '_', 'vdx'));
43
+ if (table) return { table, place: '_.vdx' };
44
+ const older = dict(at(data, 'vdx'));
45
+ return older ? { table: older, place: 'vdx' } : null;
46
+ }
47
+
48
+ /** The project's manifest and where in its mise.toml it was found. */
49
+ export function loadManifestAt(projectRoot: string): { manifest: VdxManifest; place: ManifestPlace } | null {
33
50
  const miseToml = path.join(projectRoot, 'mise.toml');
34
51
  if (!fs.existsSync(miseToml)) return null;
35
52
  try {
36
- const text = fs.readFileSync(miseToml, 'utf8');
37
- const data = TOML.parse(text) as { vdx?: VdxManifest };
38
- return data?.vdx ?? null;
53
+ const found = manifestTable(TOML.parse(fs.readFileSync(miseToml, 'utf8')));
54
+ // The fields are the project's to write; readers check what they use.
55
+ return found ? { manifest: found.table, place: found.place } : null;
39
56
  } catch {
40
57
  return null;
41
58
  }
42
59
  }
43
60
 
61
+ export function loadManifest(projectRoot: string): VdxManifest | null {
62
+ return loadManifestAt(projectRoot)?.manifest ?? null;
63
+ }
64
+
44
65
  export function loadOverrides(projectRoot: string): Override[] {
45
66
  const file = path.join(projectRoot, '.vdx-overrides.yml');
46
67
  if (!fs.existsSync(file)) return [];
package/src/mcp-server.ts CHANGED
@@ -8,12 +8,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
8
8
  import * as TOML from 'smol-toml';
9
9
 
10
10
  import { loadRubric } from './rubric.ts';
11
- import { loadManifest, loadOverrides } from './manifest.ts';
11
+ import { loadManifest, loadOverrides, manifestTable } from './manifest.ts';
12
12
  import { autoDetectStack, readToml, type Ctx } from './facts.ts';
13
13
  import { audit } from './audit.ts';
14
14
  import { reportJson, reportMarkdown } from './report.ts';
15
15
  import { DEFAULT_BASELINE, STANDARD_VERBS, type Verb } from './init.ts';
16
16
  import { resolveDefaultRubric } from './defaults.ts';
17
+ import { at, dict, errorMessage, isStringList, str, type Dict } from './narrow.ts';
17
18
 
18
19
  const DEFAULT_RUBRIC = resolveDefaultRubric();
19
20
 
@@ -30,18 +31,16 @@ function freshCtx(): Ctx {
30
31
  return { projectRoot: PROJECT_ROOT, stack, cache: new Map() };
31
32
  }
32
33
 
33
- function readMiseToml(): any {
34
+ function readMiseToml(): unknown {
34
35
  return readToml(freshCtx(), 'mise.toml');
35
36
  }
36
37
 
37
- function getVdxBlock(): any {
38
- const t = readMiseToml();
39
- return t?.vdx ?? null;
38
+ function getVdxBlock(): Dict | null {
39
+ return manifestTable(readMiseToml())?.table ?? null;
40
40
  }
41
41
 
42
- function getTasks(): Record<string, any> {
43
- const t = readMiseToml();
44
- return (t?.tasks as Record<string, any>) ?? {};
42
+ function getTasks(): Dict {
43
+ return dict(at(readMiseToml(), 'tasks')) ?? {};
45
44
  }
46
45
 
47
46
  interface RunResult {
@@ -61,10 +60,10 @@ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
61
60
  });
62
61
  let stdout = '';
63
62
  let stderr = '';
64
- child.stdout.on('data', (b) => {
63
+ child.stdout.on('data', (b: Buffer) => {
65
64
  stdout += b.toString();
66
65
  });
67
- child.stderr.on('data', (b) => {
66
+ child.stderr.on('data', (b: Buffer) => {
68
67
  stderr += b.toString();
69
68
  });
70
69
  child.on('error', (e) => {
@@ -91,33 +90,33 @@ function spawnVerb(verb: Verb, extraArgs: string[]): Promise<RunResult> {
91
90
  function buildCapabilities() {
92
91
  const vdxBlock = getVdxBlock();
93
92
  const tasks = getTasks();
94
- const verbsList = (vdxBlock?.verbs as string[] | undefined) ?? STANDARD_VERBS.slice();
93
+ const declared = vdxBlock?.['verbs'];
94
+ const verbsList = isStringList(declared) ? declared : STANDARD_VERBS.slice();
95
95
  const verbs = verbsList.map((name) => {
96
96
  const t = tasks[name];
97
- const nativeCommand =
98
- t && typeof t === 'object' && typeof t.run === 'string' ? t.run : null;
99
97
  return {
100
98
  name,
101
99
  has_manifest: Boolean(t),
102
- native_command: nativeCommand,
100
+ native_command: str(at(t, 'run')) ?? null,
103
101
  };
104
102
  });
105
103
 
106
- const sharedInfra = vdxBlock?.shared_infra
104
+ const infra = dict(vdxBlock?.['shared_infra']);
105
+ const sharedInfra = infra
107
106
  ? {
108
- provider: String(vdxBlock.shared_infra.provider ?? ''),
109
- precheck: Boolean(vdxBlock.shared_infra.precheck),
107
+ provider: str(infra['provider']) ?? '',
108
+ precheck: Boolean(infra['precheck']),
110
109
  is_running: false,
111
110
  }
112
111
  : null;
113
112
 
114
113
  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}`),
114
+ schema_version: str(vdxBlock?.['schema_version']) ?? '0.2',
115
+ stack: str(vdxBlock?.['stack']) ?? autoDetectStack(PROJECT_ROOT),
116
+ baseline: str(vdxBlock?.['baseline']) ?? `file://${DEFAULT_RUBRIC}`,
118
117
  verbs,
119
118
  shared_infra: sharedInfra,
120
- last_verified: vdxBlock?.success_path?.last_verified ?? null,
119
+ last_verified: at(vdxBlock, 'success_path', 'last_verified') ?? null,
121
120
  };
122
121
  }
123
122
 
@@ -139,40 +138,44 @@ function recordSuccessPath(input: {
139
138
  notes?: string;
140
139
  }): { ok: boolean; manifest_path: string; agents_md_updated: boolean } {
141
140
  const manifestPath = path.join(PROJECT_ROOT, 'mise.toml');
142
- let doc: any = {};
141
+ let doc: Dict = {};
143
142
  if (fs.existsSync(manifestPath)) {
144
143
  const text = fs.readFileSync(manifestPath, 'utf8');
145
144
  try {
146
145
  doc = TOML.parse(text);
147
- } catch (e: any) {
148
- throw new Error(`mise.toml parse error: ${e?.message}`);
146
+ } catch (e) {
147
+ throw new Error(`mise.toml parse error: ${errorMessage(e)}`, { cause: e });
149
148
  }
150
149
  }
151
150
 
152
- doc.tasks = doc.tasks ?? {};
151
+ const tasks = dict(doc['tasks']) ?? {};
153
152
  for (const v of input.verbs) {
154
- doc.tasks[v.name] = { run: v.command };
153
+ tasks[v.name] = { run: v.command };
155
154
  }
155
+ doc['tasks'] = tasks;
156
156
 
157
- doc.vdx = doc.vdx ?? {
157
+ // An existing manifest is updated where it is; a new one goes into [_.vdx].
158
+ const found = manifestTable(doc);
159
+ const vdx = found?.table ?? {
158
160
  schema_version: '0.2',
159
161
  baseline: DEFAULT_BASELINE,
160
162
  stack: autoDetectStack(PROJECT_ROOT),
161
163
  };
162
- doc.vdx.verbs = input.verbs.map((v) => v.name);
164
+ vdx['verbs'] = input.verbs.map((v) => v.name);
163
165
 
164
166
  if (input.shared_infra) {
165
- doc.vdx.shared_infra = {
167
+ vdx['shared_infra'] = {
166
168
  provider: input.shared_infra.provider,
167
169
  precheck: input.shared_infra.precheck,
168
170
  };
169
171
  }
170
172
 
171
- doc.vdx.success_path = {
173
+ vdx['success_path'] = {
172
174
  last_verified: new Date().toISOString().slice(0, 10),
173
175
  verified_by: 'vdx-mcp',
174
176
  ...(input.notes ? { notes: input.notes } : {}),
175
177
  };
178
+ if (!found) doc['_'] = { ...dict(doc['_']), vdx };
176
179
 
177
180
  const newText = TOML.stringify(doc);
178
181
  fs.writeFileSync(manifestPath, newText, 'utf8');
@@ -192,7 +195,7 @@ server.registerTool(
192
195
  'Возвращает manifest проекта: stack, baseline, доступные глаголы и их native команды, shared_infra и last_verified. Самый частый вызов агента, входящего в проект.',
193
196
  inputSchema: {},
194
197
  },
195
- async () => {
198
+ () => {
196
199
  const payload = buildCapabilities();
197
200
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
198
201
  },
@@ -222,7 +225,7 @@ server.registerTool(
222
225
  format: z.enum(['json', 'markdown']).optional(),
223
226
  },
224
227
  },
225
- async ({ baseline, format }) => {
228
+ ({ baseline, format }) => {
226
229
  const fmt = format ?? 'json';
227
230
  const payload = buildAuditPayload(baseline, fmt);
228
231
  return { content: [{ type: 'text', text: payload }] };
@@ -233,16 +236,14 @@ server.registerTool(
233
236
  'vdx_record_success_path',
234
237
  {
235
238
  description:
236
- 'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [vdx.success_path]. Используется после успешной верификации команд агентом.',
239
+ 'Записывает обнаруженные команды в [tasks.X] секцию mise.toml + обновляет [_.vdx.success_path] (у манифеста в старом [vdx] — там). Используется после успешной верификации команд агентом.',
237
240
  inputSchema: {
238
241
  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
+ shared_infra: z.object({ provider: z.string(), precheck: z.boolean() }).optional(),
242
243
  notes: z.string().optional(),
243
244
  },
244
245
  },
245
- async ({ verbs, shared_infra, notes }) => {
246
+ ({ verbs, shared_infra, notes }) => {
246
247
  const args: Parameters<typeof recordSuccessPath>[0] = { verbs };
247
248
  if (shared_infra) args.shared_infra = shared_infra;
248
249
  if (notes !== undefined) args.notes = notes;
@@ -257,7 +258,7 @@ async function main() {
257
258
  process.stderr.write(`vdx-mcp connected (project: ${PROJECT_ROOT})\n`);
258
259
  }
259
260
 
260
- main().catch((e) => {
261
- process.stderr.write(`vdx-mcp fatal: ${e?.message ?? e}\n`);
261
+ main().catch((e: unknown) => {
262
+ process.stderr.write(`vdx-mcp fatal: ${errorMessage(e)}\n`);
262
263
  process.exit(1);
263
264
  });