@voiden/runner 2.3.0-beta.1 → 2.3.0-beta.10

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/bundled-runners/versions.json +2 -0
  3. package/bundled-runners/voiden-faker-runner.js +6 -13
  4. package/bundled-runners/voiden-mcp-client-runner.js +1 -0
  5. package/bundled-runners/voiden-mcp-tool-runner.js +143 -0
  6. package/dist/cliPrint.d.ts +7 -0
  7. package/dist/cliPrint.d.ts.map +1 -0
  8. package/dist/cliPrint.js +166 -0
  9. package/dist/cliPrint.js.map +1 -0
  10. package/dist/envFile.d.ts +12 -1
  11. package/dist/envFile.d.ts.map +1 -1
  12. package/dist/envFile.js +77 -19
  13. package/dist/envFile.js.map +1 -1
  14. package/dist/headlessContext.d.ts.map +1 -1
  15. package/dist/headlessContext.js +34 -0
  16. package/dist/headlessContext.js.map +1 -1
  17. package/dist/index.js +268 -9
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib.d.ts +8 -1
  20. package/dist/lib.d.ts.map +1 -1
  21. package/dist/lib.js +10 -2
  22. package/dist/lib.js.map +1 -1
  23. package/dist/mcpServing.d.ts +39 -0
  24. package/dist/mcpServing.d.ts.map +1 -0
  25. package/dist/mcpServing.js +154 -0
  26. package/dist/mcpServing.js.map +1 -0
  27. package/dist/mcpToolCapability.d.ts +158 -0
  28. package/dist/mcpToolCapability.d.ts.map +1 -0
  29. package/dist/mcpToolCapability.js +87 -0
  30. package/dist/mcpToolCapability.js.map +1 -0
  31. package/dist/plugins/loader.d.ts.map +1 -1
  32. package/dist/plugins/loader.js +4 -0
  33. package/dist/plugins/loader.js.map +1 -1
  34. package/dist/runner.d.ts.map +1 -1
  35. package/dist/runner.js +7 -2
  36. package/dist/runner.js.map +1 -1
  37. package/dist/runtimeVars.d.ts.map +1 -1
  38. package/dist/runtimeVars.js +18 -0
  39. package/dist/runtimeVars.js.map +1 -1
  40. package/dist/toolRegistry.d.ts +102 -0
  41. package/dist/toolRegistry.d.ts.map +1 -0
  42. package/dist/toolRegistry.js +38 -0
  43. package/dist/toolRegistry.js.map +1 -0
  44. package/package.json +11 -5
package/dist/index.js CHANGED
@@ -6,6 +6,12 @@ import { fileURLToPath } from 'url';
6
6
  import chalk from 'chalk';
7
7
  import { runVoidFile } from './runner.js';
8
8
  import { resolveFiles } from './discovery.js';
9
+ import { discoverTools, verifyTools, validateTools, upsertToolStatus, registerToolsFromDecisions, planServedTools, getCommitSha } from './mcpToolCapability.js';
10
+ import { registerFixedTools } from './mcpServing.js';
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
14
+ import { createServer as createHttpServer } from 'node:http';
9
15
  import { loadEnabledPlugins } from './plugins/loader.js';
10
16
  import { exportToCsv } from './report/csv.js';
11
17
  import { sendMailReport } from './report/mail.js';
@@ -429,8 +435,10 @@ program
429
435
  ' voiden-runner run auth.void\n' +
430
436
  ' voiden-runner run ./requests/\n' +
431
437
  ' voiden-runner run auth.void users.void ./smoke/\n' +
432
- ' voiden-runner run ./ --env .env.staging --bail\n')
438
+ ' voiden-runner run ./ --env .env.staging --bail\n' +
439
+ ' voiden-runner run ./ --env .voiden/env-public.yaml --environment staging\n')
433
440
  .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
441
+ .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
434
442
  .option('--env-var <key=value>', 'Individual environment variable override (can be used multiple times)', (val, memo) => {
435
443
  memo.push(val);
436
444
  return memo;
@@ -470,7 +478,7 @@ program
470
478
  process.exit(EXIT_USAGE_ERROR);
471
479
  }
472
480
  try {
473
- Object.assign(env, loadEnvFile(envPath));
481
+ Object.assign(env, loadEnvFile(envPath, opts.environment));
474
482
  }
475
483
  catch (err) {
476
484
  console.error(chalk.red(` ✗ ${err.message}`));
@@ -1263,7 +1271,7 @@ pluginCmd
1263
1271
  // ── voiden-runner mcp ─────────────────────────────────────────────────────────
1264
1272
  //
1265
1273
  // Enables the AI-agent loop for CLI-only users (no Voiden app installed):
1266
- // registers @voiden/mcp-server with Claude Code / Codex, and installs a
1274
+ // registers @voiden/mcp with Claude Code / Codex, and installs a
1267
1275
  // standalone skill teaching the run/verify/write-back workflow. The Voiden
1268
1276
  // app's own Settings toggle does the equivalent for desktop users, reusing
1269
1277
  // the same registration helpers from mcpInstall.ts.
@@ -1276,23 +1284,25 @@ function resolveMcpTargets(opts) {
1276
1284
  }
1277
1285
  const mcpCmd = program
1278
1286
  .command('mcp')
1279
- .description('Enable AI-agent integration — registers @voiden/mcp-server and installs a run/verify skill');
1287
+ .description('Enable AI-agent integration — registers this project\'s fixed-tools MCP server and installs a run/verify skill');
1280
1288
  mcpCmd
1281
1289
  .command('install')
1282
- .description('Register @voiden/mcp-server with Claude Code and/or Codex, and install a skill teaching the run/verify/write-back loop.\n\n' +
1290
+ .description('Register this project with Claude Code and/or Codex points .mcp.json / config.toml at ' +
1291
+ '`voiden-runner mcp serve` (the same 4 fixed tools, standalone, no other Voiden package ' +
1292
+ 'required), and installs a skill teaching the run/verify/write-back loop.\n\n' +
1283
1293
  ' Examples:\n' +
1284
1294
  ' voiden-runner mcp install # both Claude Code and Codex\n' +
1285
1295
  ' voiden-runner mcp install --claude # Claude Code only\n' +
1286
1296
  ' voiden-runner mcp install -p ./my-project # register against a specific project dir (default: cwd)\n' +
1287
- ' voiden-runner mcp install --local-server ./dist/index.js # before publishing: point at a local build instead of npx\n')
1297
+ ' voiden-runner mcp install --local-server ./dist/index.js # point at a local build instead of npx\n')
1288
1298
  .option('--claude', 'Install for Claude Code only')
1289
1299
  .option('--codex', 'Install for Codex only')
1290
1300
  .option('-p, --project <path>', 'Project directory to register the MCP server against', '.')
1291
- .option('--local-server <path>', 'Use `node <path>` instead of `npx -y @voiden/mcp-server` — for testing against a local build before it\'s published')
1301
+ .option('--local-server <path>', 'Use `node <path> mcp serve` instead of `npx -y @voiden/runner mcp serve` — for testing against a local build')
1292
1302
  .action((opts) => {
1293
1303
  const targets = resolveMcpTargets(opts);
1294
1304
  const serverCommand = opts.localServer
1295
- ? { command: 'node', args: [resolve(opts.localServer), resolve(opts.project)] }
1305
+ ? { command: 'node', args: [resolve(opts.localServer), 'mcp', 'serve', resolve(opts.project)] }
1296
1306
  : undefined;
1297
1307
  const installed = installMcpIntegration(opts.project, targets, MCP_SKILL_MARKDOWN, serverCommand);
1298
1308
  if (installed.length === 0) {
@@ -1301,7 +1311,7 @@ mcpCmd
1301
1311
  }
1302
1312
  console.log();
1303
1313
  for (const target of installed) {
1304
- console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, @voiden/mcp-server registered for ${resolve(opts.project)}`));
1314
+ console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, fixed-tools MCP server registered for ${resolve(opts.project)}`));
1305
1315
  }
1306
1316
  if (serverCommand) {
1307
1317
  console.log(chalk.gray(` Using local build: node ${serverCommand.args[0]}`));
@@ -1342,5 +1352,254 @@ mcpCmd
1342
1352
  console.log(` server registered: ${status.codex.serverRegistered ? chalk.green('yes') : chalk.gray('no')}`);
1343
1353
  console.log();
1344
1354
  });
1355
+ mcpCmd
1356
+ .command('serve [path]')
1357
+ .description('Serve this project as an MCP server — the same tools @voiden/mcp exposes ' +
1358
+ '(list/run/write plus declared /tool capabilities), over stdio (default) or HTTP.\n\n' +
1359
+ ' Examples:\n' +
1360
+ ' voiden-runner mcp serve # stdio, current directory\n' +
1361
+ ' voiden-runner mcp serve ./api # stdio, specific project\n' +
1362
+ ' voiden-runner mcp serve --http --port 3900 # HTTP on 127.0.0.1:3900\n' +
1363
+ ' voiden-runner mcp serve --check # dry run — print what would be served, no live server\n')
1364
+ .option('--http', 'Serve over streamable HTTP instead of stdio')
1365
+ .option('-p, --port <port>', 'HTTP port (only with --http)', '3000')
1366
+ .option('--host <host>', 'HTTP bind address (only with --http) — binding beyond 127.0.0.1 is a real exposure risk', '127.0.0.1')
1367
+ .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
1368
+ .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
1369
+ .option('--check', 'Print what would be served and exit, without starting a live server')
1370
+ .action(async (path, opts) => {
1371
+ const projectRoot = resolve(path ?? '.');
1372
+ const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
1373
+ if (opts.env) {
1374
+ const envPath = resolve(opts.env);
1375
+ if (!existsSync(envPath)) {
1376
+ console.error(chalk.red(`Env file not found: ${envPath}`));
1377
+ process.exit(EXIT_USAGE_ERROR);
1378
+ }
1379
+ try {
1380
+ Object.assign(env, loadEnvFile(envPath, opts.environment));
1381
+ }
1382
+ catch (err) {
1383
+ console.error(chalk.red(` ✗ ${err.message}`));
1384
+ process.exit(EXIT_USAGE_ERROR);
1385
+ }
1386
+ }
1387
+ if (opts.check) {
1388
+ // Dry run — no live server. Same underlying decision function real
1389
+ // serving uses, so this can never disagree with what actually gets
1390
+ // registered.
1391
+ const activePlugins = await loadEnabledPlugins();
1392
+ const decisions = await planServedTools(projectRoot, env, activePlugins);
1393
+ console.log(`\n${decisions.length} /tool block(s) found in ${projectRoot}\n`);
1394
+ for (const d of decisions) {
1395
+ if (d.excluded) {
1396
+ console.log(` [EXCLUDED] ${d.tool.name}`);
1397
+ for (const reason of d.excludedReasons ?? [])
1398
+ console.log(` ${reason}`);
1399
+ continue;
1400
+ }
1401
+ const label = d.served ? (d.descriptionNote ? `SERVED (${d.status.state})` : 'SERVED') : 'WITHDRAWN';
1402
+ console.log(` [${label}] ${d.tool.name} — ${d.status.state}${d.status.note ? `: ${d.status.note}` : ''}`);
1403
+ }
1404
+ console.log(chalk.gray(` (plus the 4 fixed tools: list_void_files, list_requests, run_request, write_result)`));
1405
+ console.log();
1406
+ const anyFailing = decisions.some((d) => d.excluded || d.status?.state === 'failing');
1407
+ process.exit(anyFailing ? EXIT_RUN_FAILURE : EXIT_SUCCESS);
1408
+ }
1409
+ // Verification (real network calls) runs exactly once here, regardless
1410
+ // of transport — never repeated per HTTP request below.
1411
+ const activePlugins = await loadEnabledPlugins();
1412
+ const decisions = await planServedTools(projectRoot, env, activePlugins);
1413
+ const commitSha = getCommitSha(projectRoot);
1414
+ const servedCount = decisions.filter((d) => d.served).length;
1415
+ // Shared across calls so {{process.xxx}} runtime variables chain the
1416
+ // same way they do for the stdio path and for @voiden/mcp.
1417
+ const runtimeVars = {};
1418
+ if (opts.http) {
1419
+ const port = Number(opts.port);
1420
+ const host = opts.host;
1421
+ // A fresh McpServer + transport per HTTP request — this is how the
1422
+ // SDK's own stateless example (examples/server/simpleStatelessStreamableHttp.js)
1423
+ // does it, not an arbitrary choice: reusing one transport across
1424
+ // requests returns 500s. Cheap: registration is just schema/handler
1425
+ // wiring against the already-computed `decisions`, no re-verification.
1426
+ const httpServer = createHttpServer(async (req, res) => {
1427
+ try {
1428
+ const requestServer = new McpServer({ name: 'voiden-runner', version: '1.0.0' });
1429
+ registerFixedTools(requestServer, projectRoot, runtimeVars, activePlugins);
1430
+ registerToolsFromDecisions(requestServer, decisions, env, runtimeVars, activePlugins, commitSha, projectRoot);
1431
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
1432
+ await requestServer.connect(transport);
1433
+ res.on('close', () => {
1434
+ transport.close();
1435
+ requestServer.close();
1436
+ });
1437
+ await transport.handleRequest(req, res);
1438
+ }
1439
+ catch (err) {
1440
+ console.error(chalk.red(` ✗ Error handling MCP request: ${err?.message ?? String(err)}`));
1441
+ if (!res.headersSent) {
1442
+ res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({
1443
+ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null,
1444
+ }));
1445
+ }
1446
+ }
1447
+ });
1448
+ httpServer.listen(port, host, () => {
1449
+ console.error(chalk.green(` ✓ voiden-runner mcp serve — listening on http://${host}:${port}/mcp`));
1450
+ console.error(chalk.gray(` ${servedCount} tool(s) served (plus list_void_files, list_requests, run_request, write_result)`));
1451
+ if (host !== '127.0.0.1' && host !== 'localhost') {
1452
+ console.error(chalk.red(` ⚠ Bound to ${host} — reachable beyond this machine. Make sure that's intended.`));
1453
+ }
1454
+ });
1455
+ // No process.exit() — stays alive until Ctrl-C, same as the stdio path below.
1456
+ }
1457
+ else {
1458
+ // stdio: one persistent server for the process lifetime — stdout is
1459
+ // reserved for the JSON-RPC stream, so nothing gets printed there.
1460
+ // Startup info goes to stderr only, same discipline @voiden/mcp's
1461
+ // own entrypoint already follows (it prints nothing).
1462
+ const server = new McpServer({ name: 'voiden-runner', version: '1.0.0' });
1463
+ registerFixedTools(server, projectRoot, runtimeVars, activePlugins);
1464
+ registerToolsFromDecisions(server, decisions, env, runtimeVars, activePlugins, commitSha, projectRoot);
1465
+ await server.connect(new StdioServerTransport());
1466
+ }
1467
+ });
1468
+ // ── voiden-runner tool ────────────────────────────────────────────────────────
1469
+ //
1470
+ // Discovery + verification for /tool blocks (voiden-mcp-tool plugin) — the
1471
+ // standalone CLI surface for the same discoverTools/verifyTools functions
1472
+ // @voiden/mcp will use for live agent-serving. No scheduling here:
1473
+ // `cadence` on a verify entry is a tag `--cadence` filters by, not something
1474
+ // this command enforces timing for — that's a human/CI decision, same as
1475
+ // deciding when to run `voiden-runner run` at all.
1476
+ const toolCmd = program
1477
+ .command('tool')
1478
+ .description('Discover and verify /tool blocks — capabilities declared for AI agents');
1479
+ toolCmd
1480
+ .command('list [paths...]')
1481
+ .description('List every /tool block found under the given path(s) (default: current directory) — does not execute anything')
1482
+ .option('--json', 'Output as JSON')
1483
+ .action(async (paths, opts) => {
1484
+ const targets = paths.length > 0 ? paths : ['.'];
1485
+ const allTools = [];
1486
+ const activePlugins = await loadEnabledPlugins();
1487
+ for (const p of targets) {
1488
+ allTools.push(...await discoverTools(resolve(p), { activePlugins }));
1489
+ }
1490
+ if (opts.json) {
1491
+ console.log(JSON.stringify(allTools, null, 2));
1492
+ return;
1493
+ }
1494
+ if (allTools.length === 0) {
1495
+ console.log(chalk.yellow(' No /tool blocks found.'));
1496
+ return;
1497
+ }
1498
+ console.log();
1499
+ for (const tool of allTools) {
1500
+ console.log(chalk.bold(` ${tool.name}`) + chalk.gray(` — ${relative(process.cwd(), tool.filePath)}${tool.sectionLabel ? ` [${tool.sectionLabel}]` : ''}`));
1501
+ if (tool.description)
1502
+ console.log(chalk.gray(` ${tool.description}`));
1503
+ // onFailure is per verify-entry now, not one tool-wide setting —
1504
+ // summarize instead of showing a single (no-longer-existing) value.
1505
+ const onFailureValues = new Set(tool.verifies.map((v) => v.onFailure || 'withdraw'));
1506
+ const onFailureSummary = tool.verifies.length === 0 ? '—' : onFailureValues.size === 1 ? [...onFailureValues][0] : 'mixed';
1507
+ console.log(chalk.gray(` params: ${tool.params.length} verifies: ${tool.verifies.length} on-failure: ${onFailureSummary}`));
1508
+ console.log();
1509
+ }
1510
+ });
1511
+ toolCmd
1512
+ .command('verify [paths...]')
1513
+ .description("Run each /tool block's verification requests and report verified / unverified / failing.\n\n" +
1514
+ ' Examples:\n' +
1515
+ ' voiden-runner tool verify\n' +
1516
+ ' voiden-runner tool verify ./api/ --cadence nightly\n' +
1517
+ ' voiden-runner tool verify --json --write\n')
1518
+ .option('--cadence <tag>', 'Only run verification requests tagged with this cadence — omit to run every entry regardless of tag')
1519
+ .option('--json', 'Output as JSON (suppresses normal output — useful for CI)')
1520
+ .option('--write', 'Write the computed status back into each /tool block. Off by default — verification always recomputes fresh and never trusts a stale write-back')
1521
+ .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
1522
+ .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
1523
+ .action(async (paths, opts) => {
1524
+ const targets = paths.length > 0 ? paths : ['.'];
1525
+ const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
1526
+ if (opts.env) {
1527
+ const envPath = resolve(opts.env);
1528
+ if (!existsSync(envPath)) {
1529
+ console.error(chalk.red(`Env file not found: ${envPath}`));
1530
+ process.exit(EXIT_USAGE_ERROR);
1531
+ }
1532
+ try {
1533
+ Object.assign(env, loadEnvFile(envPath, opts.environment));
1534
+ }
1535
+ catch (err) {
1536
+ console.error(chalk.red(` ✗ ${err.message}`));
1537
+ process.exit(EXIT_USAGE_ERROR);
1538
+ }
1539
+ }
1540
+ const activePlugins = await loadEnabledPlugins();
1541
+ const allTools = [];
1542
+ for (const p of targets) {
1543
+ allTools.push(...await discoverTools(resolve(p), { activePlugins }));
1544
+ }
1545
+ if (allTools.length === 0) {
1546
+ if (opts.json)
1547
+ console.log(JSON.stringify({ tools: [], issues: [] }, null, 2));
1548
+ else
1549
+ console.log(chalk.yellow(' No /tool blocks found.'));
1550
+ process.exit(EXIT_SUCCESS);
1551
+ }
1552
+ const { validTools, issues } = await validateTools(resolve(targets[0]), allTools);
1553
+ const statuses = await verifyTools(validTools, { cadence: opts.cadence, env, activePlugins, projectRoot: resolve(targets[0]) });
1554
+ if (opts.write) {
1555
+ for (const status of statuses) {
1556
+ try {
1557
+ upsertToolStatus(status.tool.filePath, status.tool.toolBlockUid, { state: status.state, note: status.note });
1558
+ }
1559
+ catch (err) {
1560
+ console.error(chalk.red(` ✗ Failed to write status for "${status.tool.name}": ${err?.message ?? String(err)}`));
1561
+ }
1562
+ }
1563
+ }
1564
+ const anyFailing = statuses.some(s => s.state === 'failing') || issues.length > 0;
1565
+ if (opts.json) {
1566
+ console.log(JSON.stringify({ tools: statuses, issues }, null, 2));
1567
+ }
1568
+ else {
1569
+ console.log();
1570
+ // Structurally excluded tools (§1.6) — reported distinctly from a
1571
+ // verification failure: this is "the contract itself doesn't add up,"
1572
+ // not "the contract's proof failed." Neither ran nor counted below.
1573
+ const excludedNames = [...new Set(issues.map(i => i.tool.name))];
1574
+ for (const name of excludedNames) {
1575
+ console.log(` ${chalk.red('✗ excluded')} ${chalk.bold(name)}`);
1576
+ for (const issue of issues.filter(i => i.tool.name === name)) {
1577
+ console.log(chalk.gray(` [${issue.check}] ${issue.message}`));
1578
+ }
1579
+ console.log();
1580
+ }
1581
+ for (const status of statuses) {
1582
+ const icon = status.state === 'verified' ? chalk.green('✓ verified ') :
1583
+ status.state === 'failing' ? chalk.red('✗ failing ') :
1584
+ chalk.yellow('○ unverified');
1585
+ console.log(` ${icon} ${chalk.bold(status.tool.name)}` + chalk.gray(` — ${relative(process.cwd(), status.tool.filePath)}${status.tool.sectionLabel ? ` [${status.tool.sectionLabel}]` : ''}`));
1586
+ if (status.note)
1587
+ console.log(chalk.gray(` ${status.note}`));
1588
+ for (const r of status.results) {
1589
+ const rIcon = r.passed ? chalk.green('✓') : chalk.red('✗');
1590
+ console.log(` ${rIcon} ${r.entry.role}: ${r.entry.sectionLabel}` + (r.reason ? chalk.gray(` (${r.reason})`) : ''));
1591
+ }
1592
+ console.log();
1593
+ }
1594
+ const verified = statuses.filter(s => s.state === 'verified').length;
1595
+ const unverified = statuses.filter(s => s.state === 'unverified').length;
1596
+ const failing = statuses.filter(s => s.state === 'failing').length;
1597
+ console.log(chalk.bold(` ${statuses.length} tool(s)`) + chalk.gray(` — ${chalk.green(verified + ' verified')}, ${chalk.yellow(unverified + ' unverified')}, ${chalk.red(failing + ' failing')}`) + (excludedNames.length > 0 ? chalk.red(`, ${excludedNames.length} excluded`) : ''));
1598
+ if (opts.write)
1599
+ console.log(chalk.gray(' Status written back into each /tool block.'));
1600
+ console.log();
1601
+ }
1602
+ process.exit(anyFailing ? EXIT_RUN_FAILURE : EXIT_SUCCESS);
1603
+ });
1345
1604
  program.parse();
1346
1605
  //# sourceMappingURL=index.js.map