chainlesschain 0.47.8 → 0.49.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.
Files changed (86) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +10 -8
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/activitypub.js +533 -0
  10. package/src/commands/codegen.js +303 -0
  11. package/src/commands/collab.js +482 -0
  12. package/src/commands/compliance.js +597 -6
  13. package/src/commands/crosschain.js +382 -0
  14. package/src/commands/dbevo.js +388 -0
  15. package/src/commands/dev.js +411 -0
  16. package/src/commands/federation.js +427 -0
  17. package/src/commands/fusion.js +332 -0
  18. package/src/commands/governance.js +505 -0
  19. package/src/commands/hardening.js +110 -0
  20. package/src/commands/incentive.js +373 -0
  21. package/src/commands/inference.js +304 -0
  22. package/src/commands/infra.js +361 -0
  23. package/src/commands/kg.js +371 -0
  24. package/src/commands/marketplace.js +326 -0
  25. package/src/commands/matrix.js +283 -0
  26. package/src/commands/mcp.js +441 -18
  27. package/src/commands/nlprog.js +329 -0
  28. package/src/commands/nostr.js +196 -7
  29. package/src/commands/ops.js +408 -0
  30. package/src/commands/perception.js +385 -0
  31. package/src/commands/pqc.js +34 -0
  32. package/src/commands/privacy.js +345 -0
  33. package/src/commands/quantization.js +280 -0
  34. package/src/commands/recommend.js +336 -0
  35. package/src/commands/reputation.js +349 -0
  36. package/src/commands/runtime.js +500 -0
  37. package/src/commands/sla.js +352 -0
  38. package/src/commands/social.js +265 -0
  39. package/src/commands/stress.js +252 -0
  40. package/src/commands/tech.js +268 -0
  41. package/src/commands/tenant.js +576 -0
  42. package/src/commands/trust.js +366 -0
  43. package/src/harness/mcp-client.js +330 -54
  44. package/src/index.js +114 -0
  45. package/src/lib/activitypub-bridge.js +623 -0
  46. package/src/lib/aiops.js +523 -0
  47. package/src/lib/autonomous-developer.js +524 -0
  48. package/src/lib/code-agent.js +442 -0
  49. package/src/lib/collaboration-governance.js +556 -0
  50. package/src/lib/community-governance.js +649 -0
  51. package/src/lib/compliance-framework-reporter.js +600 -0
  52. package/src/lib/content-recommendation.js +600 -0
  53. package/src/lib/cross-chain.js +669 -0
  54. package/src/lib/dbevo.js +669 -0
  55. package/src/lib/decentral-infra.js +445 -0
  56. package/src/lib/federation-hardening.js +587 -0
  57. package/src/lib/hardening-manager.js +409 -0
  58. package/src/lib/inference-network.js +407 -0
  59. package/src/lib/knowledge-graph.js +530 -0
  60. package/src/lib/matrix-bridge.js +252 -0
  61. package/src/lib/mcp-client.js +3 -0
  62. package/src/lib/mcp-registry.js +347 -0
  63. package/src/lib/mcp-scaffold.js +385 -0
  64. package/src/lib/multimodal.js +698 -0
  65. package/src/lib/nl-programming.js +595 -0
  66. package/src/lib/nostr-bridge.js +214 -38
  67. package/src/lib/perception.js +500 -0
  68. package/src/lib/pqc-manager.js +141 -9
  69. package/src/lib/privacy-computing.js +575 -0
  70. package/src/lib/protocol-fusion.js +535 -0
  71. package/src/lib/quantization.js +362 -0
  72. package/src/lib/reputation-optimizer.js +509 -0
  73. package/src/lib/skill-marketplace.js +397 -0
  74. package/src/lib/sla-manager.js +484 -0
  75. package/src/lib/social-graph.js +408 -0
  76. package/src/lib/stix-parser.js +167 -0
  77. package/src/lib/stress-tester.js +383 -0
  78. package/src/lib/tech-learning-engine.js +651 -0
  79. package/src/lib/tenant-saas.js +831 -0
  80. package/src/lib/threat-intel.js +268 -0
  81. package/src/lib/token-incentive.js +513 -0
  82. package/src/lib/topic-classifier.js +400 -0
  83. package/src/lib/trust-security.js +473 -0
  84. package/src/lib/ueba.js +403 -0
  85. package/src/lib/universal-runtime.js +771 -0
  86. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -3,15 +3,32 @@
3
3
  * chainlesschain mcp servers|connect|disconnect|tools|call
4
4
  */
5
5
 
6
+ import fs from "fs";
7
+ import path from "path";
6
8
  import chalk from "chalk";
7
9
  import ora from "ora";
8
10
  import { logger } from "../lib/logger.js";
9
11
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
10
- import { MCPClient, MCPServerConfig } from "../harness/mcp-client.js";
12
+ import {
13
+ MCPClient,
14
+ MCPServerConfig,
15
+ inferTransport,
16
+ } from "../harness/mcp-client.js";
11
17
  import {
12
18
  validateMcpServer,
13
19
  annotateMcpCompatibility,
14
20
  } from "@chainlesschain/session-core";
21
+ import {
22
+ generateMcpServerScaffold,
23
+ SUPPORTED_TRANSPORTS,
24
+ } from "../lib/mcp-scaffold.js";
25
+ import {
26
+ CATALOG as REGISTRY_CATALOG,
27
+ CATEGORIES as REGISTRY_CATEGORIES,
28
+ listServers as registryListServers,
29
+ searchServers as registrySearchServers,
30
+ getServer as registryGetServer,
31
+ } from "../lib/mcp-registry.js";
15
32
 
16
33
  // Singleton MCP client for session reuse
17
34
  let mcpClient = null;
@@ -98,9 +115,15 @@ export function registerMcpCommand(program) {
98
115
  ? chalk.green(" [ok]")
99
116
  : chalk.yellow(` [blocked: ${s._reason}]`);
100
117
  logger.log(` ${chalk.cyan(s.name)}${auto}${flag}`);
101
- logger.log(
102
- ` ${chalk.gray("Command:")} ${s.command} ${s.args.join(" ")}`,
103
- );
118
+ if (s.url) {
119
+ logger.log(
120
+ ` ${chalk.gray("URL:")} ${s.url} ${chalk.gray(`[${s.transport || s._transport || "http"}]`)}`,
121
+ );
122
+ } else {
123
+ logger.log(
124
+ ` ${chalk.gray("Command:")} ${s.command} ${s.args.join(" ")}`,
125
+ );
126
+ }
104
127
  logger.log(
105
128
  ` ${chalk.gray("Compatible:")} ${s._modeCompatibility.join(", ") || "(none)"}`,
106
129
  );
@@ -119,8 +142,20 @@ export function registerMcpCommand(program) {
119
142
  .command("add")
120
143
  .description("Add or update an MCP server configuration")
121
144
  .argument("<name>", "Server name")
122
- .requiredOption("-c, --command <cmd>", "Server command to run")
145
+ .option("-c, --command <cmd>", "Server command to run (stdio transport)")
123
146
  .option("-a, --args <args>", "Command arguments (comma-separated)")
147
+ .option(
148
+ "-u, --url <url>",
149
+ "Server URL (http / https / ws / wss transports)",
150
+ )
151
+ .option(
152
+ "-t, --transport <kind>",
153
+ "Transport kind: stdio | http | https | sse | ws | wss",
154
+ )
155
+ .option(
156
+ "-H, --header <header...>",
157
+ "HTTP header to include on requests (KEY=VALUE, repeatable)",
158
+ )
124
159
  .option("--auto-connect", "Auto-connect on startup")
125
160
  .option(
126
161
  "--mode <mode>",
@@ -129,6 +164,17 @@ export function registerMcpCommand(program) {
129
164
  .option("--json", "Output as JSON")
130
165
  .action(async (name, options) => {
131
166
  try {
167
+ if (!options.command && !options.url) {
168
+ logger.error(
169
+ "Provide either -c <command> (stdio) or -u <url> (http/ws).",
170
+ );
171
+ process.exit(1);
172
+ }
173
+ if (options.command && options.url) {
174
+ logger.error("Use either -c <command> or -u <url>, not both.");
175
+ process.exit(1);
176
+ }
177
+
132
178
  const ctx = await bootstrap({ verbose: program.opts().verbose });
133
179
  if (!ctx.db) {
134
180
  logger.error("Database not available");
@@ -141,9 +187,43 @@ export function registerMcpCommand(program) {
141
187
  ? options.args.split(",").map((a) => a.trim())
142
188
  : [];
143
189
 
190
+ // Parse -H KEY=VALUE into { headers }
191
+ const headers = {};
192
+ if (Array.isArray(options.header)) {
193
+ for (const raw of options.header) {
194
+ const eq = raw.indexOf("=");
195
+ if (eq <= 0) {
196
+ logger.warn(
197
+ `Ignored malformed --header "${raw}" (expected KEY=VALUE)`,
198
+ );
199
+ continue;
200
+ }
201
+ headers[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
202
+ }
203
+ }
204
+
205
+ const transport =
206
+ options.transport ||
207
+ (options.url ? inferTransport({ url: options.url }) : "stdio");
208
+
209
+ if (options.url) {
210
+ try {
211
+ new URL(options.url);
212
+ } catch (_e) {
213
+ logger.error(`Invalid URL: ${options.url}`);
214
+ process.exit(1);
215
+ }
216
+ }
217
+
144
218
  const mode = resolveMode(options);
145
219
  const check = validateMcpServer(
146
- { name, command: options.command, args },
220
+ {
221
+ name,
222
+ command: options.command,
223
+ args,
224
+ url: options.url,
225
+ transport,
226
+ },
147
227
  mode,
148
228
  );
149
229
  if (!check.allowed) {
@@ -153,25 +233,37 @@ export function registerMcpCommand(program) {
153
233
  }
154
234
 
155
235
  config.add(name, {
156
- command: options.command,
236
+ command: options.command || null,
157
237
  args,
238
+ url: options.url || null,
239
+ transport,
240
+ env: {},
158
241
  autoConnect: !!options.autoConnect,
242
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
159
243
  });
160
244
 
245
+ const payload = {
246
+ name,
247
+ command: options.command || null,
248
+ args,
249
+ url: options.url || null,
250
+ transport,
251
+ autoConnect: !!options.autoConnect,
252
+ };
253
+
161
254
  if (options.json) {
162
- console.log(
163
- JSON.stringify({
164
- name,
165
- command: options.command,
166
- args,
167
- autoConnect: !!options.autoConnect,
168
- }),
169
- );
255
+ console.log(JSON.stringify(payload));
170
256
  } else {
171
257
  logger.success(`MCP server "${chalk.cyan(name)}" configured`);
172
- logger.log(
173
- ` ${chalk.gray("Command:")} ${options.command} ${args.join(" ")}`,
174
- );
258
+ if (options.url) {
259
+ logger.log(
260
+ ` ${chalk.gray("URL:")} ${options.url} ${chalk.gray(`[${transport}]`)}`,
261
+ );
262
+ } else {
263
+ logger.log(
264
+ ` ${chalk.gray("Command:")} ${options.command} ${args.join(" ")}`,
265
+ );
266
+ }
175
267
  }
176
268
 
177
269
  await shutdown();
@@ -381,4 +473,335 @@ export function registerMcpCommand(program) {
381
473
  process.exit(1);
382
474
  }
383
475
  });
476
+
477
+ // mcp scaffold — generate a boilerplate MCP server project
478
+ mcp
479
+ .command("scaffold <name>")
480
+ .description("Scaffold a new MCP server project (stdio or http+sse)")
481
+ .option("-d, --description <text>", "Short description of the server")
482
+ .option(
483
+ "-t, --transport <kind>",
484
+ `Transport: ${SUPPORTED_TRANSPORTS.join("|")}`,
485
+ "stdio",
486
+ )
487
+ .option("-o, --output <dir>", "Target directory (defaults to ./<name>)")
488
+ .option("-a, --author <name>", "package.json author field")
489
+ .option("-p, --port <n>", "HTTP port (http transport only)", (v) =>
490
+ parseInt(v, 10),
491
+ )
492
+ .option("--force", "Overwrite existing files")
493
+ .option("--dry-run", "Print files that would be written, don't touch disk")
494
+ .option("--json", "Output as JSON")
495
+ .action(async (name, options) => {
496
+ try {
497
+ const { files, summary } = generateMcpServerScaffold({
498
+ name,
499
+ description: options.description,
500
+ transport: options.transport,
501
+ author: options.author,
502
+ port: options.port,
503
+ });
504
+
505
+ const targetDir = path.resolve(options.output || `./${summary.name}`);
506
+
507
+ if (options.dryRun) {
508
+ if (options.json) {
509
+ console.log(JSON.stringify({ targetDir, summary, files }, null, 2));
510
+ } else {
511
+ logger.log(
512
+ `${chalk.bold("Would write")} ${files.length} files to ${chalk.cyan(targetDir)}:`,
513
+ );
514
+ for (const f of files) {
515
+ logger.log(
516
+ ` ${chalk.cyan(f.path)} ` +
517
+ chalk.dim(`(${f.content.length} bytes)`),
518
+ );
519
+ }
520
+ }
521
+ return;
522
+ }
523
+
524
+ // Collision check — refuse to clobber unless --force.
525
+ if (!options.force && fs.existsSync(targetDir)) {
526
+ const clashing = files.filter((f) =>
527
+ fs.existsSync(path.join(targetDir, f.path)),
528
+ );
529
+ if (clashing.length > 0) {
530
+ logger.error(
531
+ `Refusing to overwrite existing files in ${targetDir}: ` +
532
+ clashing.map((f) => f.path).join(", ") +
533
+ `. Re-run with --force to overwrite.`,
534
+ );
535
+ process.exit(1);
536
+ }
537
+ }
538
+
539
+ fs.mkdirSync(targetDir, { recursive: true });
540
+ for (const f of files) {
541
+ const full = path.join(targetDir, f.path);
542
+ fs.mkdirSync(path.dirname(full), { recursive: true });
543
+ fs.writeFileSync(full, f.content, "utf-8");
544
+ }
545
+
546
+ if (options.json) {
547
+ console.log(
548
+ JSON.stringify(
549
+ {
550
+ targetDir,
551
+ summary,
552
+ files: files.map((f) => f.path),
553
+ },
554
+ null,
555
+ 2,
556
+ ),
557
+ );
558
+ } else {
559
+ logger.success(
560
+ `Scaffolded ${chalk.cyan(summary.name)} ` +
561
+ chalk.dim(
562
+ `(${summary.transport}${summary.port ? `, port ${summary.port}` : ""})`,
563
+ ),
564
+ );
565
+ logger.log(` ${chalk.bold("Path:")} ${targetDir}`);
566
+ for (const f of files) {
567
+ logger.log(` ${chalk.dim("+")} ${f.path}`);
568
+ }
569
+ logger.log("");
570
+ logger.log(chalk.bold("Next steps:"));
571
+ logger.log(
572
+ ` ${chalk.dim("$")} cd ${path.relative(process.cwd(), targetDir) || "."}`,
573
+ );
574
+ logger.log(` ${chalk.dim("$")} npm install`);
575
+ if (summary.transport === "stdio") {
576
+ logger.log(
577
+ ` ${chalk.dim("$")} cc mcp add ${summary.name} -c node -a "./index.js"`,
578
+ );
579
+ } else {
580
+ logger.log(` ${chalk.dim("$")} npm start`);
581
+ logger.log(
582
+ ` ${chalk.dim("$")} cc mcp add ${summary.name} -u http://localhost:${summary.port}/mcp`,
583
+ );
584
+ }
585
+ }
586
+ } catch (err) {
587
+ logger.error(`Scaffold failed: ${err.message}`);
588
+ process.exit(1);
589
+ }
590
+ });
591
+
592
+ // mcp registry — browse + search + one-shot install from the bundled catalog
593
+ const registry = mcp
594
+ .command("registry")
595
+ .description("Browse the curated catalog of community MCP servers");
596
+
597
+ registry
598
+ .command("list")
599
+ .description("List catalog entries (filter by category/tag/author)")
600
+ .option("-c, --category <name>", "Filter by category")
601
+ .option("-t, --tags <list>", "Filter by comma-separated tags (any match)")
602
+ .option("--author <name>", "Filter by author (substring, case-insensitive)")
603
+ .option("--sort <field>", "Sort by 'name' | 'rating' | 'category'", "name")
604
+ .option("--order <dir>", "'asc' | 'desc'", "asc")
605
+ .option("--limit <n>", "Max results", (v) => parseInt(v, 10))
606
+ .option("--offset <n>", "Pagination offset", (v) => parseInt(v, 10))
607
+ .option("--json", "Output as JSON")
608
+ .action((options) => {
609
+ try {
610
+ const tags = options.tags
611
+ ? options.tags
612
+ .split(",")
613
+ .map((t) => t.trim())
614
+ .filter(Boolean)
615
+ : undefined;
616
+ const { servers, total } = registryListServers({
617
+ category: options.category,
618
+ tags,
619
+ author: options.author,
620
+ sortBy: options.sort,
621
+ sortOrder: options.order,
622
+ limit: options.limit,
623
+ offset: options.offset,
624
+ });
625
+
626
+ if (options.json) {
627
+ console.log(JSON.stringify({ servers, total }, null, 2));
628
+ return;
629
+ }
630
+
631
+ if (servers.length === 0) {
632
+ logger.info("No catalog entries match your filters.");
633
+ return;
634
+ }
635
+
636
+ logger.log(
637
+ chalk.bold(`MCP Registry — ${servers.length}/${total} servers\n`),
638
+ );
639
+ for (const s of servers) {
640
+ logger.log(
641
+ ` ${chalk.cyan(s.name)} ${chalk.gray(`(${s.id})`)} ` +
642
+ chalk.dim(`★${s.rating ?? "-"} ${s.category}`),
643
+ );
644
+ logger.log(` ${chalk.gray(s.description)}`);
645
+ if (s.tags?.length) {
646
+ logger.log(
647
+ ` ${chalk.gray("tags:")} ${s.tags.slice(0, 5).join(", ")}`,
648
+ );
649
+ }
650
+ }
651
+ } catch (err) {
652
+ logger.error(`Registry list failed: ${err.message}`);
653
+ process.exit(1);
654
+ }
655
+ });
656
+
657
+ registry
658
+ .command("search <keyword>")
659
+ .description("Keyword search across name/description/tags")
660
+ .option("--json", "Output as JSON")
661
+ .action((keyword, options) => {
662
+ try {
663
+ const hits = registrySearchServers(keyword);
664
+ if (options.json) {
665
+ console.log(JSON.stringify({ hits, total: hits.length }, null, 2));
666
+ return;
667
+ }
668
+ if (hits.length === 0) {
669
+ logger.info(`No matches for "${keyword}".`);
670
+ return;
671
+ }
672
+ logger.log(chalk.bold(`${hits.length} matches for "${keyword}":\n`));
673
+ for (const s of hits) {
674
+ logger.log(
675
+ ` ${chalk.cyan(s.name)} ${chalk.gray(`(${s.id})`)} ` +
676
+ chalk.dim(s.category),
677
+ );
678
+ logger.log(` ${chalk.gray(s.description)}`);
679
+ }
680
+ } catch (err) {
681
+ logger.error(`Registry search failed: ${err.message}`);
682
+ process.exit(1);
683
+ }
684
+ });
685
+
686
+ registry
687
+ .command("show <idOrName>")
688
+ .description("Show full catalog entry (id or short name)")
689
+ .option("--json", "Output as JSON")
690
+ .action((idOrName, options) => {
691
+ try {
692
+ const entry = registryGetServer(idOrName);
693
+ if (!entry) {
694
+ logger.error(`Not found: "${idOrName}".`);
695
+ process.exit(1);
696
+ }
697
+ if (options.json) {
698
+ console.log(JSON.stringify(entry, null, 2));
699
+ return;
700
+ }
701
+ logger.log(
702
+ `${chalk.bold(entry.displayName)} ${chalk.gray(`(${entry.id})`)}`,
703
+ );
704
+ logger.log(` ${chalk.gray("Author:")} ${entry.author}`);
705
+ logger.log(` ${chalk.gray("Category:")} ${entry.category}`);
706
+ logger.log(` ${chalk.gray("Version:")} ${entry.version}`);
707
+ logger.log(` ${chalk.gray("Rating:")} ★${entry.rating ?? "-"}`);
708
+ logger.log(` ${chalk.gray("Package:")} ${entry.npmPackage}`);
709
+ logger.log(
710
+ ` ${chalk.gray("Command:")} ${entry.command} ${entry.args.join(" ")}`,
711
+ );
712
+ logger.log(` ${chalk.gray("Transport:")}${entry.transport}`);
713
+ if (entry.homepage) {
714
+ logger.log(` ${chalk.gray("Homepage:")} ${entry.homepage}`);
715
+ }
716
+ logger.log(`\n ${entry.description}`);
717
+ if (entry.tools?.length) {
718
+ logger.log(`\n ${chalk.bold("Tools:")} ${entry.tools.join(", ")}`);
719
+ }
720
+ logger.log(
721
+ `\n ${chalk.dim("Install:")} cc mcp registry install ${entry.name}`,
722
+ );
723
+ } catch (err) {
724
+ logger.error(`Registry show failed: ${err.message}`);
725
+ process.exit(1);
726
+ }
727
+ });
728
+
729
+ registry
730
+ .command("install <idOrName>")
731
+ .description("Install a catalog entry by registering it as an MCP server")
732
+ .option("--as <name>", "Override the stored server name")
733
+ .option("--auto-connect", "Mark the server to auto-connect on startup")
734
+ .option("--json", "Output as JSON")
735
+ .action(async (idOrName, options) => {
736
+ try {
737
+ const entry = registryGetServer(idOrName);
738
+ if (!entry) {
739
+ logger.error(
740
+ `Not found: "${idOrName}". Try 'cc mcp registry list' or 'search'.`,
741
+ );
742
+ process.exit(1);
743
+ }
744
+
745
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
746
+ if (!ctx.db) {
747
+ logger.error("Database not available");
748
+ process.exit(1);
749
+ }
750
+
751
+ const db = ctx.db.getDatabase();
752
+ const config = new MCPServerConfig(db);
753
+ const storedName = (options.as || entry.name).trim();
754
+
755
+ config.add(storedName, {
756
+ command: entry.command,
757
+ args: entry.args,
758
+ autoConnect: !!options.autoConnect,
759
+ });
760
+
761
+ if (options.json) {
762
+ console.log(
763
+ JSON.stringify(
764
+ {
765
+ name: storedName,
766
+ id: entry.id,
767
+ command: entry.command,
768
+ args: entry.args,
769
+ autoConnect: !!options.autoConnect,
770
+ },
771
+ null,
772
+ 2,
773
+ ),
774
+ );
775
+ } else {
776
+ logger.success(
777
+ `Installed ${chalk.cyan(storedName)} ` + chalk.dim(`(${entry.id})`),
778
+ );
779
+ logger.log(
780
+ ` ${chalk.gray("Command:")} ${entry.command} ${entry.args.join(" ")}`,
781
+ );
782
+ logger.log(` ${chalk.gray("Next:")} cc mcp connect ${storedName}`);
783
+ }
784
+
785
+ await shutdown();
786
+ } catch (err) {
787
+ logger.error(`Registry install failed: ${err.message}`);
788
+ process.exit(1);
789
+ }
790
+ });
791
+
792
+ registry
793
+ .command("categories")
794
+ .description("List available registry categories")
795
+ .option("--json", "Output as JSON")
796
+ .action((options) => {
797
+ if (options.json) {
798
+ console.log(JSON.stringify(REGISTRY_CATEGORIES, null, 2));
799
+ return;
800
+ }
801
+ logger.log(chalk.bold("Categories:"));
802
+ for (const c of REGISTRY_CATEGORIES) {
803
+ const count = REGISTRY_CATALOG.filter((s) => s.category === c).length;
804
+ logger.log(` ${chalk.cyan(c)} ${chalk.dim(`(${count})`)}`);
805
+ }
806
+ });
384
807
  }