kairn-cli 1.3.0 → 1.5.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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/cli.ts
2
- import { Command as Command7 } from "commander";
2
+ import { Command as Command10 } from "commander";
3
3
 
4
4
  // src/commands/init.ts
5
5
  import { Command } from "commander";
@@ -8,6 +8,9 @@ import chalk from "chalk";
8
8
  import Anthropic from "@anthropic-ai/sdk";
9
9
  import OpenAI from "openai";
10
10
  import { execFileSync } from "child_process";
11
+ import fs2 from "fs/promises";
12
+ import path2 from "path";
13
+ import { fileURLToPath } from "url";
11
14
 
12
15
  // src/config.ts
13
16
  import fs from "fs/promises";
@@ -16,15 +19,24 @@ import os from "os";
16
19
  var KAIRN_DIR = path.join(os.homedir(), ".kairn");
17
20
  var CONFIG_PATH = path.join(KAIRN_DIR, "config.json");
18
21
  var ENVS_DIR = path.join(KAIRN_DIR, "envs");
22
+ var TEMPLATES_DIR = path.join(KAIRN_DIR, "templates");
23
+ var USER_REGISTRY_PATH = path.join(KAIRN_DIR, "user-registry.json");
19
24
  function getConfigPath() {
20
25
  return CONFIG_PATH;
21
26
  }
22
27
  function getEnvsDir() {
23
28
  return ENVS_DIR;
24
29
  }
30
+ function getTemplatesDir() {
31
+ return TEMPLATES_DIR;
32
+ }
33
+ function getUserRegistryPath() {
34
+ return USER_REGISTRY_PATH;
35
+ }
25
36
  async function ensureDirs() {
26
37
  await fs.mkdir(KAIRN_DIR, { recursive: true });
27
38
  await fs.mkdir(ENVS_DIR, { recursive: true });
39
+ await fs.mkdir(TEMPLATES_DIR, { recursive: true });
28
40
  }
29
41
  async function loadConfig() {
30
42
  try {
@@ -50,6 +62,42 @@ async function saveConfig(config) {
50
62
  }
51
63
 
52
64
  // src/commands/init.ts
65
+ var __filename = fileURLToPath(import.meta.url);
66
+ var __dirname = path2.dirname(__filename);
67
+ async function installSeedTemplates() {
68
+ const templatesDir = getTemplatesDir();
69
+ await fs2.mkdir(templatesDir, { recursive: true });
70
+ const candidates = [
71
+ path2.resolve(__dirname, "../registry/templates"),
72
+ path2.resolve(__dirname, "../src/registry/templates"),
73
+ path2.resolve(__dirname, "../../src/registry/templates")
74
+ ];
75
+ let seedDir = null;
76
+ for (const candidate of candidates) {
77
+ try {
78
+ await fs2.access(candidate);
79
+ seedDir = candidate;
80
+ break;
81
+ } catch {
82
+ continue;
83
+ }
84
+ }
85
+ if (!seedDir) return;
86
+ const files = (await fs2.readdir(seedDir)).filter((f) => f.endsWith(".json"));
87
+ let installed = 0;
88
+ for (const file of files) {
89
+ const dest = path2.join(templatesDir, file);
90
+ try {
91
+ await fs2.access(dest);
92
+ } catch {
93
+ await fs2.copyFile(path2.join(seedDir, file), dest);
94
+ installed++;
95
+ }
96
+ }
97
+ if (installed > 0) {
98
+ console.log(chalk.green(` \u2713 ${installed} template${installed === 1 ? "" : "s"} installed`));
99
+ }
100
+ }
53
101
  var PROVIDER_MODELS = {
54
102
  anthropic: {
55
103
  name: "Anthropic",
@@ -171,6 +219,7 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
171
219
  console.log(
172
220
  chalk.dim(` \u2713 Provider: ${providerInfo.name}, Model: ${model}`)
173
221
  );
222
+ await installSeedTemplates();
174
223
  const hasClaude = detectClaudeCode();
175
224
  if (hasClaude) {
176
225
  console.log(chalk.green(" \u2713 Claude Code detected"));
@@ -190,13 +239,10 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
190
239
  import { Command as Command2 } from "commander";
191
240
  import { input, confirm } from "@inquirer/prompts";
192
241
  import chalk2 from "chalk";
193
- import fs4 from "fs/promises";
194
- import path4 from "path";
195
242
 
196
243
  // src/compiler/compile.ts
197
- import fs2 from "fs/promises";
198
- import path2 from "path";
199
- import { fileURLToPath } from "url";
244
+ import fs4 from "fs/promises";
245
+ import path4 from "path";
200
246
  import crypto from "crypto";
201
247
  import Anthropic2 from "@anthropic-ai/sdk";
202
248
  import OpenAI2 from "openai";
@@ -261,6 +307,7 @@ Do not add generic filler. Every line must be specific to the user's workflow.
261
307
  9. settings.json with deny rules for \`rm -rf\`, \`curl|sh\`, reading \`.env\` and \`secrets/\`
262
308
  10. A \`/project:status\` command for code projects (uses ! for live git/test output)
263
309
  11. A \`/project:fix\` command for code projects (uses $ARGUMENTS for issue number)
310
+ 12. A \`docs/SPRINT.md\` file for sprint contracts (acceptance criteria, verification steps)
264
311
 
265
312
  ## Shell-Integrated Commands
266
313
 
@@ -349,6 +396,26 @@ Generate hooks in settings.json based on project type:
349
396
 
350
397
  Merge hooks into the \`settings\` object alongside permissions. Choose the formatter hook based on detected dependencies (Prettier \u2192 prettier, ESLint \u2192 eslint, Black \u2192 black).
351
398
 
399
+ ## PostCompact Hook
400
+
401
+ All projects should include a PostCompact hook to restore context after compaction:
402
+
403
+ \`\`\`json
404
+ {
405
+ "hooks": {
406
+ "PostCompact": [{
407
+ "matcher": "",
408
+ "hooks": [{
409
+ "type": "prompt",
410
+ "prompt": "Re-read CLAUDE.md and docs/SPRINT.md (if it exists) to restore project context after compaction."
411
+ }]
412
+ }]
413
+ }
414
+ }
415
+ \`\`\`
416
+
417
+ Merge this into the settings hooks alongside the PreToolUse and PostToolUse hooks.
418
+
352
419
  ## Tool Selection Rules
353
420
 
354
421
  - Only select tools directly relevant to the described workflow
@@ -358,6 +425,19 @@ Merge hooks into the \`settings\` object alongside permissions. Choose the forma
358
425
  - Maximum 6-8 MCP servers to avoid context bloat
359
426
  - Include a \`reason\` for each selected tool explaining why it fits this workflow
360
427
 
428
+ ## Context Budget (STRICT)
429
+
430
+ - MCP servers: maximum 6. Prefer fewer.
431
+ - CLAUDE.md: maximum 100 lines.
432
+ - Rules: maximum 5 files, each under 20 lines.
433
+ - Skills: maximum 3. Only include directly relevant ones.
434
+ - Agents: maximum 3. QA pipeline + one specialist.
435
+ - Commands: no limit (loaded on demand, zero context cost).
436
+ - Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
437
+
438
+ If the workflow doesn't clearly need a tool, DO NOT include it.
439
+ Each MCP server costs 500-2000 tokens of context window.
440
+
361
441
  ## For Code Projects, Additionally Include
362
442
 
363
443
  - \`/project:plan\` command (plan before coding)
@@ -366,12 +446,16 @@ Merge hooks into the \`settings\` object alongside permissions. Choose the forma
366
446
  - \`/project:commit\` command (conventional commits)
367
447
  - \`/project:status\` command (live git status, recent commits, TODO overview using ! prefix)
368
448
  - \`/project:fix\` command (takes $ARGUMENTS as issue number, plans fix, implements, tests, commits)
449
+ - \`/project:sprint\` command (define acceptance criteria before coding, writes to docs/SPRINT.md)
369
450
  - A TDD skill using the 3-phase isolation pattern (RED \u2192 GREEN \u2192 REFACTOR):
370
451
  - RED: Write failing test only. Verify it FAILS.
371
452
  - GREEN: Write MINIMUM code to pass. Nothing extra.
372
453
  - REFACTOR: Improve while keeping tests green.
373
454
  Rules: never write tests and implementation in same step, AAA pattern, one assertion per test.
374
- - A reviewer agent (read-only, Sonnet model)
455
+ - A multi-agent QA pipeline:
456
+ - \`@qa-orchestrator\` (sonnet) \u2014 delegates to linter and e2e-tester, compiles QA report
457
+ - \`@linter\` (haiku) \u2014 runs formatters, linters, security scanners
458
+ - \`@e2e-tester\` (sonnet, only when Playwright is in tools) \u2014 browser-based QA via Playwright
375
459
 
376
460
  ## For Research Projects, Additionally Include
377
461
 
@@ -386,6 +470,15 @@ Merge hooks into the \`settings\` object alongside permissions. Choose the forma
386
470
  - \`/project:edit\` command (review and improve writing)
387
471
  - A writing-workflow skill
388
472
 
473
+ ## Hermes Runtime
474
+
475
+ When generating for Hermes runtime, the same EnvironmentSpec JSON is produced. The adapter layer handles conversion:
476
+ - MCP config entries \u2192 Hermes config.yaml mcp_servers
477
+ - Commands and skills \u2192 ~/.hermes/skills/ markdown files
478
+ - Rules \u2192 ~/.hermes/skills/rule-*.md files
479
+
480
+ The LLM output format does not change. Adapter-level conversion happens post-compilation.
481
+
389
482
  ## Output Schema
390
483
 
391
484
  Return ONLY valid JSON matching this structure:
@@ -412,7 +505,8 @@ Return ONLY valid JSON matching this structure:
412
505
  "help": "markdown content for /project:help",
413
506
  "tasks": "markdown content for /project:tasks",
414
507
  "status": "Show project status:\\n\\n!git status --short\\n\\n!git log --oneline -5\\n\\nRead TODO.md and summarize progress.",
415
- "fix": "Fix issue #$ARGUMENTS:\\n\\n1. Read the issue and understand the problem\\n2. Plan the fix\\n3. Implement the fix\\n4. Run tests:\\n\\n!npm test 2>&1 | tail -20\\n\\n5. Commit with: fix: resolve #$ARGUMENTS"
508
+ "fix": "Fix issue #$ARGUMENTS:\\n\\n1. Read the issue and understand the problem\\n2. Plan the fix\\n3. Implement the fix\\n4. Run tests:\\n\\n!npm test 2>&1 | tail -20\\n\\n5. Commit with: fix: resolve #$ARGUMENTS",
509
+ "sprint": "Define a sprint contract for the next feature:\\n\\n1. Read docs/TODO.md for context:\\n\\n!cat docs/TODO.md 2>/dev/null\\n\\n2. Write a CONTRACT to docs/SPRINT.md with: feature name, acceptance criteria, verification steps, files to modify, scope estimate.\\n3. Do NOT start coding until contract is confirmed."
416
510
  },
417
511
  "rules": {
418
512
  "continuity": "markdown content for continuity rule",
@@ -422,12 +516,15 @@ Return ONLY valid JSON matching this structure:
422
516
  "skill-name/SKILL": "markdown content with YAML frontmatter"
423
517
  },
424
518
  "agents": {
425
- "agent-name": "markdown content with YAML frontmatter"
519
+ "qa-orchestrator": "---\\nname: qa-orchestrator\\ndescription: Orchestrates QA pipeline\\nmodel: sonnet\\n---\\nRun QA: delegate to @linter for static analysis, @e2e-tester for browser tests. Compile consolidated report.",
520
+ "linter": "---\\nname: linter\\ndescription: Fast static analysis\\nmodel: haiku\\n---\\nRun available linters (eslint, prettier, biome, ruff, mypy, semgrep). Report issues.",
521
+ "e2e-tester": "---\\nname: e2e-tester\\ndescription: Browser-based QA via Playwright\\nmodel: sonnet\\n---\\nTest user flows via Playwright. Verify behavior, not just DOM. Screenshot failures."
426
522
  },
427
523
  "docs": {
428
524
  "TODO": "# TODO\\n\\n- [ ] First task based on workflow",
429
525
  "DECISIONS": "# Decisions\\n\\nArchitectural decisions for this project.",
430
- "LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries and gotchas."
526
+ "LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries and gotchas.",
527
+ "SPRINT": "# Sprint Contract\\n\\nDefine acceptance criteria before starting work."
431
528
  }
432
529
  }
433
530
  }
@@ -435,18 +532,21 @@ Return ONLY valid JSON matching this structure:
435
532
 
436
533
  Do not include any text outside the JSON object. Do not wrap in markdown code fences.`;
437
534
 
438
- // src/compiler/compile.ts
439
- async function loadRegistry() {
440
- const __filename = fileURLToPath(import.meta.url);
441
- const __dirname = path2.dirname(__filename);
535
+ // src/registry/loader.ts
536
+ import fs3 from "fs/promises";
537
+ import path3 from "path";
538
+ import { fileURLToPath as fileURLToPath2 } from "url";
539
+ var __filename2 = fileURLToPath2(import.meta.url);
540
+ var __dirname2 = path3.dirname(__filename2);
541
+ async function loadBundledRegistry() {
442
542
  const candidates = [
443
- path2.resolve(__dirname, "../registry/tools.json"),
444
- path2.resolve(__dirname, "../src/registry/tools.json"),
445
- path2.resolve(__dirname, "../../src/registry/tools.json")
543
+ path3.resolve(__dirname2, "../registry/tools.json"),
544
+ path3.resolve(__dirname2, "../src/registry/tools.json"),
545
+ path3.resolve(__dirname2, "../../src/registry/tools.json")
446
546
  ];
447
547
  for (const candidate of candidates) {
448
548
  try {
449
- const data = await fs2.readFile(candidate, "utf-8");
549
+ const data = await fs3.readFile(candidate, "utf-8");
450
550
  return JSON.parse(data);
451
551
  } catch {
452
552
  continue;
@@ -454,6 +554,32 @@ async function loadRegistry() {
454
554
  }
455
555
  throw new Error("Could not find tools.json registry");
456
556
  }
557
+ async function loadUserRegistry() {
558
+ try {
559
+ const data = await fs3.readFile(getUserRegistryPath(), "utf-8");
560
+ return JSON.parse(data);
561
+ } catch {
562
+ return [];
563
+ }
564
+ }
565
+ async function saveUserRegistry(tools) {
566
+ await fs3.writeFile(getUserRegistryPath(), JSON.stringify(tools, null, 2), "utf-8");
567
+ }
568
+ async function loadRegistry() {
569
+ const bundled = await loadBundledRegistry();
570
+ const user = await loadUserRegistry();
571
+ if (user.length === 0) return bundled;
572
+ const merged = /* @__PURE__ */ new Map();
573
+ for (const tool of bundled) {
574
+ merged.set(tool.id, tool);
575
+ }
576
+ for (const tool of user) {
577
+ merged.set(tool.id, tool);
578
+ }
579
+ return Array.from(merged.values());
580
+ }
581
+
582
+ // src/compiler/compile.ts
457
583
  function buildUserMessage(intent, registry) {
458
584
  const registrySummary = registry.map(
459
585
  (t) => `- ${t.id} (${t.type}, tier ${t.tier}, auth: ${t.auth}): ${t.description} [best_for: ${t.best_for.join(", ")}]`
@@ -563,6 +689,24 @@ async function callLLM(config, userMessage) {
563
689
  }
564
690
  throw new Error(`Unsupported provider: ${config.provider}. Run \`kairn init\` to reconfigure.`);
565
691
  }
692
+ function validateSpec(spec, onProgress) {
693
+ const warnings = [];
694
+ if (spec.tools.length > 8) {
695
+ warnings.push(`${spec.tools.length} MCP servers selected (recommended: \u22646)`);
696
+ }
697
+ if (spec.harness.claude_md) {
698
+ const lines = spec.harness.claude_md.split("\n").length;
699
+ if (lines > 150) {
700
+ warnings.push(`CLAUDE.md is ${lines} lines (recommended: \u2264100)`);
701
+ }
702
+ }
703
+ if (spec.harness.skills && Object.keys(spec.harness.skills).length > 5) {
704
+ warnings.push(`${Object.keys(spec.harness.skills).length} skills (recommended: \u22643)`);
705
+ }
706
+ for (const warning of warnings) {
707
+ onProgress?.(`\u26A0 ${warning}`);
708
+ }
709
+ }
566
710
  async function compile(intent, onProgress) {
567
711
  const config = await loadConfig();
568
712
  if (!config) {
@@ -581,69 +725,114 @@ async function compile(intent, onProgress) {
581
725
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
582
726
  ...parsed
583
727
  };
728
+ validateSpec(spec, onProgress);
584
729
  await ensureDirs();
585
- const envPath = path2.join(getEnvsDir(), `${spec.id}.json`);
586
- await fs2.writeFile(envPath, JSON.stringify(spec, null, 2), "utf-8");
730
+ const envPath = path4.join(getEnvsDir(), `${spec.id}.json`);
731
+ await fs4.writeFile(envPath, JSON.stringify(spec, null, 2), "utf-8");
587
732
  return spec;
588
733
  }
589
734
 
590
735
  // src/adapter/claude-code.ts
591
- import fs3 from "fs/promises";
592
- import path3 from "path";
736
+ import fs5 from "fs/promises";
737
+ import path5 from "path";
593
738
  async function writeFile(filePath, content) {
594
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
595
- await fs3.writeFile(filePath, content, "utf-8");
739
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
740
+ await fs5.writeFile(filePath, content, "utf-8");
741
+ }
742
+ function buildFileMap(spec) {
743
+ const files = /* @__PURE__ */ new Map();
744
+ if (spec.harness.claude_md) {
745
+ files.set(".claude/CLAUDE.md", spec.harness.claude_md);
746
+ }
747
+ if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
748
+ files.set(
749
+ ".claude/settings.json",
750
+ JSON.stringify(spec.harness.settings, null, 2)
751
+ );
752
+ }
753
+ if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
754
+ files.set(
755
+ ".mcp.json",
756
+ JSON.stringify({ mcpServers: spec.harness.mcp_config }, null, 2)
757
+ );
758
+ }
759
+ if (spec.harness.commands) {
760
+ for (const [name, content] of Object.entries(spec.harness.commands)) {
761
+ files.set(`.claude/commands/${name}.md`, content);
762
+ }
763
+ }
764
+ if (spec.harness.rules) {
765
+ for (const [name, content] of Object.entries(spec.harness.rules)) {
766
+ files.set(`.claude/rules/${name}.md`, content);
767
+ }
768
+ }
769
+ if (spec.harness.skills) {
770
+ for (const [skillPath, content] of Object.entries(spec.harness.skills)) {
771
+ files.set(`.claude/skills/${skillPath}.md`, content);
772
+ }
773
+ }
774
+ if (spec.harness.agents) {
775
+ for (const [name, content] of Object.entries(spec.harness.agents)) {
776
+ files.set(`.claude/agents/${name}.md`, content);
777
+ }
778
+ }
779
+ if (spec.harness.docs) {
780
+ for (const [name, content] of Object.entries(spec.harness.docs)) {
781
+ files.set(`.claude/docs/${name}.md`, content);
782
+ }
783
+ }
784
+ return files;
596
785
  }
597
786
  async function writeEnvironment(spec, targetDir) {
598
- const claudeDir = path3.join(targetDir, ".claude");
787
+ const claudeDir = path5.join(targetDir, ".claude");
599
788
  const written = [];
600
789
  if (spec.harness.claude_md) {
601
- const p = path3.join(claudeDir, "CLAUDE.md");
790
+ const p = path5.join(claudeDir, "CLAUDE.md");
602
791
  await writeFile(p, spec.harness.claude_md);
603
792
  written.push(".claude/CLAUDE.md");
604
793
  }
605
794
  if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
606
- const p = path3.join(claudeDir, "settings.json");
795
+ const p = path5.join(claudeDir, "settings.json");
607
796
  await writeFile(p, JSON.stringify(spec.harness.settings, null, 2));
608
797
  written.push(".claude/settings.json");
609
798
  }
610
799
  if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
611
- const p = path3.join(targetDir, ".mcp.json");
800
+ const p = path5.join(targetDir, ".mcp.json");
612
801
  const mcpContent = { mcpServers: spec.harness.mcp_config };
613
802
  await writeFile(p, JSON.stringify(mcpContent, null, 2));
614
803
  written.push(".mcp.json");
615
804
  }
616
805
  if (spec.harness.commands) {
617
806
  for (const [name, content] of Object.entries(spec.harness.commands)) {
618
- const p = path3.join(claudeDir, "commands", `${name}.md`);
807
+ const p = path5.join(claudeDir, "commands", `${name}.md`);
619
808
  await writeFile(p, content);
620
809
  written.push(`.claude/commands/${name}.md`);
621
810
  }
622
811
  }
623
812
  if (spec.harness.rules) {
624
813
  for (const [name, content] of Object.entries(spec.harness.rules)) {
625
- const p = path3.join(claudeDir, "rules", `${name}.md`);
814
+ const p = path5.join(claudeDir, "rules", `${name}.md`);
626
815
  await writeFile(p, content);
627
816
  written.push(`.claude/rules/${name}.md`);
628
817
  }
629
818
  }
630
819
  if (spec.harness.skills) {
631
820
  for (const [skillPath, content] of Object.entries(spec.harness.skills)) {
632
- const p = path3.join(claudeDir, "skills", `${skillPath}.md`);
821
+ const p = path5.join(claudeDir, "skills", `${skillPath}.md`);
633
822
  await writeFile(p, content);
634
823
  written.push(`.claude/skills/${skillPath}.md`);
635
824
  }
636
825
  }
637
826
  if (spec.harness.agents) {
638
827
  for (const [name, content] of Object.entries(spec.harness.agents)) {
639
- const p = path3.join(claudeDir, "agents", `${name}.md`);
828
+ const p = path5.join(claudeDir, "agents", `${name}.md`);
640
829
  await writeFile(p, content);
641
830
  written.push(`.claude/agents/${name}.md`);
642
831
  }
643
832
  }
644
833
  if (spec.harness.docs) {
645
834
  for (const [name, content] of Object.entries(spec.harness.docs)) {
646
- const p = path3.join(claudeDir, "docs", `${name}.md`);
835
+ const p = path5.join(claudeDir, "docs", `${name}.md`);
647
836
  await writeFile(p, content);
648
837
  written.push(`.claude/docs/${name}.md`);
649
838
  }
@@ -681,27 +870,131 @@ function summarizeSpec(spec, registry) {
681
870
  };
682
871
  }
683
872
 
684
- // src/commands/describe.ts
685
- import { fileURLToPath as fileURLToPath2 } from "url";
686
- async function loadRegistry2() {
687
- const __filename = fileURLToPath2(import.meta.url);
688
- const __dirname = path4.dirname(__filename);
689
- const candidates = [
690
- path4.resolve(__dirname, "../registry/tools.json"),
691
- path4.resolve(__dirname, "../src/registry/tools.json"),
692
- path4.resolve(__dirname, "../../src/registry/tools.json")
693
- ];
694
- for (const candidate of candidates) {
695
- try {
696
- const data = await fs4.readFile(candidate, "utf-8");
697
- return JSON.parse(data);
698
- } catch {
699
- continue;
873
+ // src/adapter/hermes-agent.ts
874
+ import fs6 from "fs/promises";
875
+ import path6 from "path";
876
+ import os2 from "os";
877
+ async function writeFile2(filePath, content) {
878
+ await fs6.mkdir(path6.dirname(filePath), { recursive: true });
879
+ await fs6.writeFile(filePath, content, "utf-8");
880
+ }
881
+ function toYaml(obj, indent = 0) {
882
+ const pad = " ".repeat(indent);
883
+ if (obj === null || obj === void 0) {
884
+ return "~";
885
+ }
886
+ if (typeof obj === "boolean") {
887
+ return obj ? "true" : "false";
888
+ }
889
+ if (typeof obj === "number") {
890
+ return String(obj);
891
+ }
892
+ if (typeof obj === "string") {
893
+ const needsQuotes = obj === "" || /[:#\[\]{}&*!|>'"%@`,]/.test(obj) || /^(true|false|null|~|\d)/.test(obj) || obj.includes("\n");
894
+ return needsQuotes ? `"${obj.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : obj;
895
+ }
896
+ if (Array.isArray(obj)) {
897
+ if (obj.length === 0) {
898
+ return "[]";
700
899
  }
900
+ return obj.map((item) => `${pad}- ${toYaml(item, indent + 1).trimStart()}`).join("\n");
701
901
  }
702
- throw new Error("Could not find tools.json registry");
902
+ if (typeof obj === "object") {
903
+ const entries = Object.entries(obj);
904
+ if (entries.length === 0) {
905
+ return "{}";
906
+ }
907
+ return entries.map(([key, value]) => {
908
+ const valueStr = toYaml(value, indent + 1);
909
+ const isScalar = typeof value !== "object" || value === null || Array.isArray(value);
910
+ if (isScalar && !Array.isArray(value)) {
911
+ return `${pad}${key}: ${valueStr}`;
912
+ }
913
+ if (Array.isArray(value)) {
914
+ if (value.length === 0) {
915
+ return `${pad}${key}: []`;
916
+ }
917
+ return `${pad}${key}:
918
+ ${valueStr}`;
919
+ }
920
+ return `${pad}${key}:
921
+ ${valueStr}`;
922
+ }).join("\n");
923
+ }
924
+ return String(obj);
703
925
  }
704
- var describeCommand = new Command2("describe").description("Describe your workflow and generate a Claude Code environment").argument("[intent]", "What you want your agent to do").option("-y, --yes", "Skip confirmation prompt").action(async (intentArg, options) => {
926
+ function buildMcpServersYaml(spec, registry) {
927
+ const servers = {};
928
+ for (const selected of spec.tools) {
929
+ const tool = registry.find((t) => t.id === selected.tool_id);
930
+ if (!tool) continue;
931
+ if (tool.install.hermes?.mcp_server) {
932
+ const serverName = tool.id.replace(/_/g, "-");
933
+ servers[serverName] = tool.install.hermes.mcp_server;
934
+ } else if (tool.install.mcp_config) {
935
+ for (const [serverName, serverConfig] of Object.entries(
936
+ tool.install.mcp_config
937
+ )) {
938
+ servers[serverName] = serverConfig;
939
+ }
940
+ }
941
+ }
942
+ for (const [serverName, serverConfig] of Object.entries(
943
+ spec.harness.mcp_config || {}
944
+ )) {
945
+ if (!(serverName in servers)) {
946
+ servers[serverName] = serverConfig;
947
+ }
948
+ }
949
+ if (Object.keys(servers).length === 0) {
950
+ return "";
951
+ }
952
+ const lines = [];
953
+ lines.push(`# Generated by Kairn v1.5.0`);
954
+ lines.push(`# Environment: ${spec.name}`);
955
+ lines.push(``);
956
+ lines.push(`mcp_servers:`);
957
+ for (const [serverName, serverConfig] of Object.entries(servers)) {
958
+ lines.push(` ${serverName}:`);
959
+ lines.push(toYaml(serverConfig, 2));
960
+ }
961
+ return lines.join("\n") + "\n";
962
+ }
963
+ async function writeHermesEnvironment(spec, registry) {
964
+ const hermesDir = path6.join(os2.homedir(), ".hermes");
965
+ const written = [];
966
+ const configYaml = buildMcpServersYaml(spec, registry);
967
+ if (configYaml) {
968
+ const configPath = path6.join(hermesDir, "config.yaml");
969
+ await writeFile2(configPath, configYaml);
970
+ written.push(".hermes/config.yaml");
971
+ }
972
+ if (spec.harness.commands) {
973
+ for (const [name, content] of Object.entries(spec.harness.commands)) {
974
+ const skillPath = path6.join(hermesDir, "skills", `${name}.md`);
975
+ await writeFile2(skillPath, content);
976
+ written.push(`.hermes/skills/${name}.md`);
977
+ }
978
+ }
979
+ if (spec.harness.skills) {
980
+ for (const [name, content] of Object.entries(spec.harness.skills)) {
981
+ const skillPath = path6.join(hermesDir, "skills", `${name}.md`);
982
+ await writeFile2(skillPath, content);
983
+ written.push(`.hermes/skills/${name}.md`);
984
+ }
985
+ }
986
+ if (spec.harness.rules) {
987
+ for (const [name, content] of Object.entries(spec.harness.rules)) {
988
+ const skillPath = path6.join(hermesDir, "skills", `rule-${name}.md`);
989
+ await writeFile2(skillPath, content);
990
+ written.push(`.hermes/skills/rule-${name}.md`);
991
+ }
992
+ }
993
+ return written;
994
+ }
995
+
996
+ // src/commands/describe.ts
997
+ var describeCommand = new Command2("describe").description("Describe your workflow and generate a Claude Code environment").argument("[intent]", "What you want your agent to do").option("-y, --yes", "Skip confirmation prompt").option("--runtime <runtime>", "Target runtime (claude-code or hermes)", "claude-code").action(async (intentArg, options) => {
705
998
  const config = await loadConfig();
706
999
  if (!config) {
707
1000
  console.log(
@@ -731,7 +1024,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
731
1024
  `));
732
1025
  process.exit(1);
733
1026
  }
734
- const registry = await loadRegistry2();
1027
+ const registry = await loadRegistry();
735
1028
  const summary = summarizeSpec(spec, registry);
736
1029
  console.log(chalk2.green("\n \u2713 Environment compiled\n"));
737
1030
  console.log(chalk2.cyan(" Name: ") + spec.name);
@@ -765,46 +1058,53 @@ var describeCommand = new Command2("describe").description("Describe your workfl
765
1058
  return;
766
1059
  }
767
1060
  const targetDir = process.cwd();
768
- const written = await writeEnvironment(spec, targetDir);
769
- console.log(chalk2.green("\n \u2713 Environment written\n"));
770
- for (const file of written) {
771
- console.log(chalk2.dim(` ${file}`));
772
- }
773
- if (summary.envSetup.length > 0) {
774
- console.log(chalk2.yellow("\n API keys needed (set these environment variables):\n"));
775
- const seen = /* @__PURE__ */ new Set();
776
- for (const env of summary.envSetup) {
777
- if (seen.has(env.envVar)) continue;
778
- seen.add(env.envVar);
779
- console.log(chalk2.bold(` export ${env.envVar}="your-key-here"`));
780
- console.log(chalk2.dim(` ${env.description}`));
781
- if (env.signupUrl) {
782
- console.log(chalk2.dim(` Get one at: ${env.signupUrl}`));
1061
+ const runtime = options.runtime ?? "claude-code";
1062
+ if (runtime === "hermes") {
1063
+ await writeHermesEnvironment(spec, registry);
1064
+ console.log(chalk2.green("\n \u2713 Environment written for Hermes\n"));
1065
+ console.log(chalk2.cyan("\n Ready! Run ") + chalk2.bold("hermes") + chalk2.cyan(" to start.\n"));
1066
+ } else {
1067
+ const written = await writeEnvironment(spec, targetDir);
1068
+ console.log(chalk2.green("\n \u2713 Environment written\n"));
1069
+ for (const file of written) {
1070
+ console.log(chalk2.dim(` ${file}`));
1071
+ }
1072
+ if (summary.envSetup.length > 0) {
1073
+ console.log(chalk2.yellow("\n API keys needed (set these environment variables):\n"));
1074
+ const seen = /* @__PURE__ */ new Set();
1075
+ for (const env of summary.envSetup) {
1076
+ if (seen.has(env.envVar)) continue;
1077
+ seen.add(env.envVar);
1078
+ console.log(chalk2.bold(` export ${env.envVar}="your-key-here"`));
1079
+ console.log(chalk2.dim(` ${env.description}`));
1080
+ if (env.signupUrl) {
1081
+ console.log(chalk2.dim(` Get one at: ${env.signupUrl}`));
1082
+ }
1083
+ console.log("");
783
1084
  }
784
- console.log("");
785
1085
  }
786
- }
787
- if (summary.pluginCommands.length > 0) {
788
- console.log(chalk2.yellow(" Install plugins by running these in Claude Code:"));
789
- for (const cmd of summary.pluginCommands) {
790
- console.log(chalk2.bold(` ${cmd}`));
1086
+ if (summary.pluginCommands.length > 0) {
1087
+ console.log(chalk2.yellow(" Install plugins by running these in Claude Code:"));
1088
+ for (const cmd of summary.pluginCommands) {
1089
+ console.log(chalk2.bold(` ${cmd}`));
1090
+ }
791
1091
  }
1092
+ console.log(
1093
+ chalk2.cyan("\n Ready! Run ") + chalk2.bold("claude") + chalk2.cyan(" to start.\n")
1094
+ );
792
1095
  }
793
- console.log(
794
- chalk2.cyan("\n Ready! Run ") + chalk2.bold("claude") + chalk2.cyan(" to start.\n")
795
- );
796
1096
  });
797
1097
 
798
1098
  // src/commands/list.ts
799
1099
  import { Command as Command3 } from "commander";
800
1100
  import chalk3 from "chalk";
801
- import fs5 from "fs/promises";
802
- import path5 from "path";
1101
+ import fs7 from "fs/promises";
1102
+ import path7 from "path";
803
1103
  var listCommand = new Command3("list").description("Show saved environments").action(async () => {
804
1104
  const envsDir = getEnvsDir();
805
1105
  let files;
806
1106
  try {
807
- files = await fs5.readdir(envsDir);
1107
+ files = await fs7.readdir(envsDir);
808
1108
  } catch {
809
1109
  console.log(chalk3.dim("\n No environments yet. Run ") + chalk3.bold("kairn describe") + chalk3.dim(" to create one.\n"));
810
1110
  return;
@@ -817,7 +1117,7 @@ var listCommand = new Command3("list").description("Show saved environments").ac
817
1117
  console.log(chalk3.cyan("\n Saved Environments\n"));
818
1118
  for (const file of jsonFiles) {
819
1119
  try {
820
- const data = await fs5.readFile(path5.join(envsDir, file), "utf-8");
1120
+ const data = await fs7.readFile(path7.join(envsDir, file), "utf-8");
821
1121
  const spec = JSON.parse(data);
822
1122
  const date = new Date(spec.created_at).toLocaleDateString();
823
1123
  const toolCount = spec.tools?.length ?? 0;
@@ -835,30 +1135,49 @@ var listCommand = new Command3("list").description("Show saved environments").ac
835
1135
  // src/commands/activate.ts
836
1136
  import { Command as Command4 } from "commander";
837
1137
  import chalk4 from "chalk";
838
- import fs6 from "fs/promises";
839
- import path6 from "path";
1138
+ import fs8 from "fs/promises";
1139
+ import path8 from "path";
840
1140
  var activateCommand = new Command4("activate").description("Re-deploy a saved environment to the current directory").argument("<env_id>", "Environment ID (from kairn list)").action(async (envId) => {
841
1141
  const envsDir = getEnvsDir();
842
- let files;
1142
+ const templatesDir = getTemplatesDir();
1143
+ let sourceDir;
1144
+ let match;
1145
+ let fromTemplate = false;
1146
+ let envFiles = [];
843
1147
  try {
844
- files = await fs6.readdir(envsDir);
1148
+ envFiles = await fs8.readdir(envsDir);
845
1149
  } catch {
846
- console.log(chalk4.red("\n No saved environments found.\n"));
847
- process.exit(1);
848
1150
  }
849
- const match = files.find(
1151
+ match = envFiles.find(
850
1152
  (f) => f === `${envId}.json` || f.startsWith(envId)
851
1153
  );
852
- if (!match) {
853
- console.log(chalk4.red(`
1154
+ if (match) {
1155
+ sourceDir = envsDir;
1156
+ } else {
1157
+ let templateFiles = [];
1158
+ try {
1159
+ templateFiles = await fs8.readdir(templatesDir);
1160
+ } catch {
1161
+ }
1162
+ match = templateFiles.find(
1163
+ (f) => f === `${envId}.json` || f.startsWith(envId)
1164
+ );
1165
+ if (match) {
1166
+ sourceDir = templatesDir;
1167
+ fromTemplate = true;
1168
+ } else {
1169
+ console.log(chalk4.red(`
854
1170
  Environment "${envId}" not found.`));
855
- console.log(chalk4.dim(" Run kairn list to see saved environments.\n"));
856
- process.exit(1);
1171
+ console.log(chalk4.dim(" Run kairn list to see saved environments."));
1172
+ console.log(chalk4.dim(" Run kairn templates to see available templates.\n"));
1173
+ process.exit(1);
1174
+ }
857
1175
  }
858
- const data = await fs6.readFile(path6.join(envsDir, match), "utf-8");
1176
+ const data = await fs8.readFile(path8.join(sourceDir, match), "utf-8");
859
1177
  const spec = JSON.parse(data);
1178
+ const label = fromTemplate ? chalk4.dim(" (template)") : "";
860
1179
  console.log(chalk4.cyan(`
861
- Activating: ${spec.name}`));
1180
+ Activating: ${spec.name}`) + label);
862
1181
  console.log(chalk4.dim(` ${spec.description}
863
1182
  `));
864
1183
  const targetDir = process.cwd();
@@ -875,21 +1194,21 @@ var activateCommand = new Command4("activate").description("Re-deploy a saved en
875
1194
  // src/commands/update-registry.ts
876
1195
  import { Command as Command5 } from "commander";
877
1196
  import chalk5 from "chalk";
878
- import fs7 from "fs/promises";
879
- import path7 from "path";
1197
+ import fs9 from "fs/promises";
1198
+ import path9 from "path";
880
1199
  import { fileURLToPath as fileURLToPath3 } from "url";
881
1200
  var REGISTRY_URL = "https://raw.githubusercontent.com/ashtonperlroth/kairn/main/src/registry/tools.json";
882
1201
  async function getLocalRegistryPath() {
883
- const __filename = fileURLToPath3(import.meta.url);
884
- const __dirname = path7.dirname(__filename);
1202
+ const __filename3 = fileURLToPath3(import.meta.url);
1203
+ const __dirname3 = path9.dirname(__filename3);
885
1204
  const candidates = [
886
- path7.resolve(__dirname, "../registry/tools.json"),
887
- path7.resolve(__dirname, "../src/registry/tools.json"),
888
- path7.resolve(__dirname, "../../src/registry/tools.json")
1205
+ path9.resolve(__dirname3, "../registry/tools.json"),
1206
+ path9.resolve(__dirname3, "../src/registry/tools.json"),
1207
+ path9.resolve(__dirname3, "../../src/registry/tools.json")
889
1208
  ];
890
1209
  for (const candidate of candidates) {
891
1210
  try {
892
- await fs7.access(candidate);
1211
+ await fs9.access(candidate);
893
1212
  return candidate;
894
1213
  } catch {
895
1214
  continue;
@@ -930,10 +1249,10 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
930
1249
  const registryPath = await getLocalRegistryPath();
931
1250
  const backupPath = registryPath + ".bak";
932
1251
  try {
933
- await fs7.copyFile(registryPath, backupPath);
1252
+ await fs9.copyFile(registryPath, backupPath);
934
1253
  } catch {
935
1254
  }
936
- await fs7.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
1255
+ await fs9.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
937
1256
  console.log(chalk5.green(` \u2713 Registry updated: ${tools.length} tools`));
938
1257
  console.log(chalk5.dim(` Saved to: ${registryPath}`));
939
1258
  console.log(chalk5.dim(` Backup: ${backupPath}
@@ -949,16 +1268,15 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
949
1268
  import { Command as Command6 } from "commander";
950
1269
  import { confirm as confirm2 } from "@inquirer/prompts";
951
1270
  import chalk6 from "chalk";
952
- import fs9 from "fs/promises";
953
- import path9 from "path";
954
- import { fileURLToPath as fileURLToPath4 } from "url";
1271
+ import fs11 from "fs/promises";
1272
+ import path11 from "path";
955
1273
 
956
1274
  // src/scanner/scan.ts
957
- import fs8 from "fs/promises";
958
- import path8 from "path";
1275
+ import fs10 from "fs/promises";
1276
+ import path10 from "path";
959
1277
  async function fileExists(p) {
960
1278
  try {
961
- await fs8.access(p);
1279
+ await fs10.access(p);
962
1280
  return true;
963
1281
  } catch {
964
1282
  return false;
@@ -966,7 +1284,7 @@ async function fileExists(p) {
966
1284
  }
967
1285
  async function readJsonSafe(p) {
968
1286
  try {
969
- const data = await fs8.readFile(p, "utf-8");
1287
+ const data = await fs10.readFile(p, "utf-8");
970
1288
  return JSON.parse(data);
971
1289
  } catch {
972
1290
  return null;
@@ -974,14 +1292,14 @@ async function readJsonSafe(p) {
974
1292
  }
975
1293
  async function readFileSafe(p) {
976
1294
  try {
977
- return await fs8.readFile(p, "utf-8");
1295
+ return await fs10.readFile(p, "utf-8");
978
1296
  } catch {
979
1297
  return null;
980
1298
  }
981
1299
  }
982
1300
  async function listDirSafe(p) {
983
1301
  try {
984
- const entries = await fs8.readdir(p);
1302
+ const entries = await fs10.readdir(p);
985
1303
  return entries.filter((e) => !e.startsWith("."));
986
1304
  } catch {
987
1305
  return [];
@@ -1033,7 +1351,7 @@ function extractEnvKeys(content) {
1033
1351
  return keys;
1034
1352
  }
1035
1353
  async function scanProject(dir) {
1036
- const pkg = await readJsonSafe(path8.join(dir, "package.json"));
1354
+ const pkg = await readJsonSafe(path10.join(dir, "package.json"));
1037
1355
  const deps = pkg?.dependencies ? Object.keys(pkg.dependencies) : [];
1038
1356
  const devDeps = pkg?.devDependencies ? Object.keys(pkg.devDependencies) : [];
1039
1357
  const allDeps = [...deps, ...devDeps];
@@ -1061,19 +1379,19 @@ async function scanProject(dir) {
1061
1379
  const framework = detectFramework(allDeps);
1062
1380
  const typescript = keyFiles.includes("tsconfig.json") || allDeps.includes("typescript");
1063
1381
  const testCommand = scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1' ? scripts.test : null;
1064
- const hasTests = testCommand !== null || await fileExists(path8.join(dir, "tests")) || await fileExists(path8.join(dir, "__tests__")) || await fileExists(path8.join(dir, "test"));
1382
+ const hasTests = testCommand !== null || await fileExists(path10.join(dir, "tests")) || await fileExists(path10.join(dir, "__tests__")) || await fileExists(path10.join(dir, "test"));
1065
1383
  const buildCommand = scripts.build || null;
1066
1384
  const lintCommand = scripts.lint || null;
1067
- const hasSrc = await fileExists(path8.join(dir, "src"));
1068
- const hasDocker = await fileExists(path8.join(dir, "docker-compose.yml")) || await fileExists(path8.join(dir, "Dockerfile"));
1069
- const hasCi = await fileExists(path8.join(dir, ".github/workflows"));
1070
- const hasEnvFile = await fileExists(path8.join(dir, ".env")) || await fileExists(path8.join(dir, ".env.example"));
1385
+ const hasSrc = await fileExists(path10.join(dir, "src"));
1386
+ const hasDocker = await fileExists(path10.join(dir, "docker-compose.yml")) || await fileExists(path10.join(dir, "Dockerfile"));
1387
+ const hasCi = await fileExists(path10.join(dir, ".github/workflows"));
1388
+ const hasEnvFile = await fileExists(path10.join(dir, ".env")) || await fileExists(path10.join(dir, ".env.example"));
1071
1389
  let envKeys = [];
1072
- const envExample = await readFileSafe(path8.join(dir, ".env.example"));
1390
+ const envExample = await readFileSafe(path10.join(dir, ".env.example"));
1073
1391
  if (envExample) {
1074
1392
  envKeys = extractEnvKeys(envExample);
1075
1393
  }
1076
- const claudeDir = path8.join(dir, ".claude");
1394
+ const claudeDir = path10.join(dir, ".claude");
1077
1395
  const hasClaudeDir = await fileExists(claudeDir);
1078
1396
  let existingClaudeMd = null;
1079
1397
  let existingSettings = null;
@@ -1085,21 +1403,21 @@ async function scanProject(dir) {
1085
1403
  let mcpServerCount = 0;
1086
1404
  let claudeMdLineCount = 0;
1087
1405
  if (hasClaudeDir) {
1088
- existingClaudeMd = await readFileSafe(path8.join(claudeDir, "CLAUDE.md"));
1406
+ existingClaudeMd = await readFileSafe(path10.join(claudeDir, "CLAUDE.md"));
1089
1407
  if (existingClaudeMd) {
1090
1408
  claudeMdLineCount = existingClaudeMd.split("\n").length;
1091
1409
  }
1092
- existingSettings = await readJsonSafe(path8.join(claudeDir, "settings.json"));
1093
- existingMcpConfig = await readJsonSafe(path8.join(dir, ".mcp.json"));
1410
+ existingSettings = await readJsonSafe(path10.join(claudeDir, "settings.json"));
1411
+ existingMcpConfig = await readJsonSafe(path10.join(dir, ".mcp.json"));
1094
1412
  if (existingMcpConfig?.mcpServers) {
1095
1413
  mcpServerCount = Object.keys(existingMcpConfig.mcpServers).length;
1096
1414
  }
1097
- existingCommands = (await listDirSafe(path8.join(claudeDir, "commands"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1098
- existingRules = (await listDirSafe(path8.join(claudeDir, "rules"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1099
- existingSkills = await listDirSafe(path8.join(claudeDir, "skills"));
1100
- existingAgents = (await listDirSafe(path8.join(claudeDir, "agents"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1415
+ existingCommands = (await listDirSafe(path10.join(claudeDir, "commands"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1416
+ existingRules = (await listDirSafe(path10.join(claudeDir, "rules"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1417
+ existingSkills = await listDirSafe(path10.join(claudeDir, "skills"));
1418
+ existingAgents = (await listDirSafe(path10.join(claudeDir, "agents"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1101
1419
  }
1102
- const name = pkg?.name || path8.basename(dir);
1420
+ const name = pkg?.name || path10.basename(dir);
1103
1421
  const description = pkg?.description || "";
1104
1422
  return {
1105
1423
  name,
@@ -1135,23 +1453,57 @@ async function scanProject(dir) {
1135
1453
  }
1136
1454
 
1137
1455
  // src/commands/optimize.ts
1138
- async function loadRegistry3() {
1139
- const __filename = fileURLToPath4(import.meta.url);
1140
- const __dirname = path9.dirname(__filename);
1141
- const candidates = [
1142
- path9.resolve(__dirname, "../registry/tools.json"),
1143
- path9.resolve(__dirname, "../src/registry/tools.json"),
1144
- path9.resolve(__dirname, "../../src/registry/tools.json")
1145
- ];
1146
- for (const candidate of candidates) {
1456
+ function simpleDiff(oldContent, newContent) {
1457
+ const oldLines = oldContent.split("\n");
1458
+ const newLines = newContent.split("\n");
1459
+ const output = [];
1460
+ const maxLines = Math.max(oldLines.length, newLines.length);
1461
+ for (let i = 0; i < maxLines; i++) {
1462
+ const oldLine = oldLines[i];
1463
+ const newLine = newLines[i];
1464
+ if (oldLine === void 0) {
1465
+ output.push(chalk6.green(`+ ${newLine}`));
1466
+ } else if (newLine === void 0) {
1467
+ output.push(chalk6.red(`- ${oldLine}`));
1468
+ } else if (oldLine !== newLine) {
1469
+ output.push(chalk6.red(`- ${oldLine}`));
1470
+ output.push(chalk6.green(`+ ${newLine}`));
1471
+ }
1472
+ }
1473
+ return output;
1474
+ }
1475
+ async function generateDiff(spec, targetDir) {
1476
+ const fileMap = buildFileMap(spec);
1477
+ const results = [];
1478
+ for (const [relativePath, newContent] of fileMap) {
1479
+ const absolutePath = path11.join(targetDir, relativePath);
1480
+ let oldContent = null;
1147
1481
  try {
1148
- const data = await fs9.readFile(candidate, "utf-8");
1149
- return JSON.parse(data);
1482
+ oldContent = await fs11.readFile(absolutePath, "utf-8");
1150
1483
  } catch {
1151
- continue;
1484
+ }
1485
+ if (oldContent === null) {
1486
+ results.push({
1487
+ path: relativePath,
1488
+ status: "new",
1489
+ diff: chalk6.green("+ NEW FILE")
1490
+ });
1491
+ } else if (oldContent === newContent) {
1492
+ results.push({
1493
+ path: relativePath,
1494
+ status: "unchanged",
1495
+ diff: ""
1496
+ });
1497
+ } else {
1498
+ const diffLines = simpleDiff(oldContent, newContent);
1499
+ results.push({
1500
+ path: relativePath,
1501
+ status: "modified",
1502
+ diff: diffLines.join("\n")
1503
+ });
1152
1504
  }
1153
1505
  }
1154
- throw new Error("Could not find tools.json registry");
1506
+ return results;
1155
1507
  }
1156
1508
  function buildProfileSummary(profile) {
1157
1509
  const lines = [];
@@ -1231,7 +1583,7 @@ ${profile.existingClaudeMd}`);
1231
1583
  }
1232
1584
  return parts.join("\n");
1233
1585
  }
1234
- var optimizeCommand = new Command6("optimize").description("Scan an existing project and generate or optimize its Claude Code environment").option("-y, --yes", "Skip confirmation prompts").option("--audit-only", "Only audit the existing harness, don't generate changes").action(async (options) => {
1586
+ var optimizeCommand = new Command6("optimize").description("Scan an existing project and generate or optimize its Claude Code environment").option("-y, --yes", "Skip confirmation prompts").option("--audit-only", "Only audit the existing harness, don't generate changes").option("--diff", "Preview changes as a diff without writing").option("--runtime <runtime>", "Target runtime (claude-code or hermes)", "claude-code").action(async (options) => {
1235
1587
  const config = await loadConfig();
1236
1588
  if (!config) {
1237
1589
  console.log(
@@ -1322,7 +1674,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1322
1674
  `));
1323
1675
  process.exit(1);
1324
1676
  }
1325
- const registry = await loadRegistry3();
1677
+ const registry = await loadRegistry();
1326
1678
  const summary = summarizeSpec(spec, registry);
1327
1679
  console.log(chalk6.green(" \u2713 Environment compiled\n"));
1328
1680
  console.log(chalk6.cyan(" Name: ") + spec.name);
@@ -1345,46 +1697,511 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1345
1697
  console.log(chalk6.yellow(` ${cmd}`));
1346
1698
  }
1347
1699
  }
1348
- const written = await writeEnvironment(spec, targetDir);
1349
- console.log(chalk6.green("\n \u2713 Environment written\n"));
1350
- for (const file of written) {
1351
- console.log(chalk6.dim(` ${file}`));
1352
- }
1353
- if (summary.envSetup.length > 0) {
1354
- console.log(chalk6.yellow("\n API keys needed (set these environment variables):\n"));
1355
- const seen = /* @__PURE__ */ new Set();
1356
- for (const env of summary.envSetup) {
1357
- if (seen.has(env.envVar)) continue;
1358
- seen.add(env.envVar);
1359
- console.log(chalk6.bold(` export ${env.envVar}="your-key-here"`));
1360
- console.log(chalk6.dim(` ${env.description}`));
1361
- if (env.signupUrl) {
1362
- console.log(chalk6.dim(` Get one at: ${env.signupUrl}`));
1700
+ if (options.diff) {
1701
+ const diffs = await generateDiff(spec, targetDir);
1702
+ const changedDiffs = diffs.filter((d) => d.status !== "unchanged");
1703
+ if (changedDiffs.length === 0) {
1704
+ console.log(chalk6.green("\n \u2713 No changes needed \u2014 environment is already up to date.\n"));
1705
+ return;
1706
+ }
1707
+ console.log(chalk6.cyan("\n Changes preview:\n"));
1708
+ for (const d of changedDiffs) {
1709
+ console.log(chalk6.cyan(` --- ${d.path}`));
1710
+ if (d.status === "new") {
1711
+ console.log(` ${d.diff}`);
1712
+ } else {
1713
+ for (const line of d.diff.split("\n")) {
1714
+ console.log(` ${line}`);
1715
+ }
1363
1716
  }
1364
1717
  console.log("");
1365
1718
  }
1719
+ const apply = await confirm2({
1720
+ message: "Apply these changes?",
1721
+ default: true
1722
+ });
1723
+ if (!apply) {
1724
+ console.log(chalk6.dim("\n Aborted.\n"));
1725
+ return;
1726
+ }
1366
1727
  }
1367
- if (summary.pluginCommands.length > 0) {
1368
- console.log(chalk6.yellow(" Install plugins by running these in Claude Code:"));
1369
- for (const cmd of summary.pluginCommands) {
1370
- console.log(chalk6.bold(` ${cmd}`));
1728
+ const runtime = options.runtime ?? "claude-code";
1729
+ if (runtime === "hermes") {
1730
+ await writeHermesEnvironment(spec, registry);
1731
+ console.log(chalk6.green("\n \u2713 Environment written for Hermes\n"));
1732
+ console.log(chalk6.cyan("\n Ready! Run ") + chalk6.bold("hermes") + chalk6.cyan(" to start.\n"));
1733
+ } else {
1734
+ const written = await writeEnvironment(spec, targetDir);
1735
+ console.log(chalk6.green("\n \u2713 Environment written\n"));
1736
+ for (const file of written) {
1737
+ console.log(chalk6.dim(` ${file}`));
1738
+ }
1739
+ if (summary.envSetup.length > 0) {
1740
+ console.log(chalk6.yellow("\n API keys needed (set these environment variables):\n"));
1741
+ const seen = /* @__PURE__ */ new Set();
1742
+ for (const env of summary.envSetup) {
1743
+ if (seen.has(env.envVar)) continue;
1744
+ seen.add(env.envVar);
1745
+ console.log(chalk6.bold(` export ${env.envVar}="your-key-here"`));
1746
+ console.log(chalk6.dim(` ${env.description}`));
1747
+ if (env.signupUrl) {
1748
+ console.log(chalk6.dim(` Get one at: ${env.signupUrl}`));
1749
+ }
1750
+ console.log("");
1751
+ }
1752
+ }
1753
+ if (summary.pluginCommands.length > 0) {
1754
+ console.log(chalk6.yellow(" Install plugins by running these in Claude Code:"));
1755
+ for (const cmd of summary.pluginCommands) {
1756
+ console.log(chalk6.bold(` ${cmd}`));
1757
+ }
1758
+ }
1759
+ console.log(
1760
+ chalk6.cyan("\n Ready! Run ") + chalk6.bold("claude") + chalk6.cyan(" to start.\n")
1761
+ );
1762
+ }
1763
+ });
1764
+
1765
+ // src/commands/doctor.ts
1766
+ import { Command as Command7 } from "commander";
1767
+ import chalk7 from "chalk";
1768
+ function runChecks(profile) {
1769
+ const checks = [];
1770
+ if (!profile.existingClaudeMd) {
1771
+ checks.push({
1772
+ name: "CLAUDE.md",
1773
+ weight: 3,
1774
+ status: "fail",
1775
+ message: "Missing CLAUDE.md"
1776
+ });
1777
+ } else if (profile.claudeMdLineCount > 200) {
1778
+ checks.push({
1779
+ name: "CLAUDE.md",
1780
+ weight: 2,
1781
+ status: "warn",
1782
+ message: `${profile.claudeMdLineCount} lines (recommended: \u2264100)`
1783
+ });
1784
+ } else {
1785
+ checks.push({
1786
+ name: "CLAUDE.md",
1787
+ weight: 3,
1788
+ status: "pass",
1789
+ message: `${profile.claudeMdLineCount} lines`
1790
+ });
1791
+ }
1792
+ if (!profile.existingSettings) {
1793
+ checks.push({
1794
+ name: "settings.json",
1795
+ weight: 2,
1796
+ status: "fail",
1797
+ message: "Missing settings.json"
1798
+ });
1799
+ } else {
1800
+ const perms2 = profile.existingSettings.permissions;
1801
+ const hasDeny = perms2?.deny && Array.isArray(perms2.deny) && perms2.deny.length > 0;
1802
+ checks.push({
1803
+ name: "Deny rules",
1804
+ weight: 2,
1805
+ status: hasDeny ? "pass" : "warn",
1806
+ message: hasDeny ? "Deny rules configured" : "No deny rules in settings.json"
1807
+ });
1808
+ }
1809
+ if (profile.mcpServerCount > 8) {
1810
+ checks.push({
1811
+ name: "MCP servers",
1812
+ weight: 1,
1813
+ status: "warn",
1814
+ message: `${profile.mcpServerCount} servers (recommended: \u22648)`
1815
+ });
1816
+ } else if (profile.mcpServerCount > 0) {
1817
+ checks.push({
1818
+ name: "MCP servers",
1819
+ weight: 1,
1820
+ status: "pass",
1821
+ message: `${profile.mcpServerCount} servers`
1822
+ });
1823
+ } else {
1824
+ checks.push({
1825
+ name: "MCP servers",
1826
+ weight: 1,
1827
+ status: "warn",
1828
+ message: "No MCP servers configured"
1829
+ });
1830
+ }
1831
+ checks.push({
1832
+ name: "/project:help",
1833
+ weight: 2,
1834
+ status: profile.existingCommands.includes("help") ? "pass" : "fail",
1835
+ message: profile.existingCommands.includes("help") ? "Help command present" : "Missing /project:help command"
1836
+ });
1837
+ checks.push({
1838
+ name: "/project:tasks",
1839
+ weight: 1,
1840
+ status: profile.existingCommands.includes("tasks") ? "pass" : "warn",
1841
+ message: profile.existingCommands.includes("tasks") ? "Tasks command present" : "Missing /project:tasks command"
1842
+ });
1843
+ checks.push({
1844
+ name: "Security rule",
1845
+ weight: 3,
1846
+ status: profile.existingRules.includes("security") ? "pass" : "fail",
1847
+ message: profile.existingRules.includes("security") ? "Security rule present" : "Missing rules/security.md"
1848
+ });
1849
+ checks.push({
1850
+ name: "Continuity rule",
1851
+ weight: 2,
1852
+ status: profile.existingRules.includes("continuity") ? "pass" : "warn",
1853
+ message: profile.existingRules.includes("continuity") ? "Continuity rule present" : "Missing rules/continuity.md"
1854
+ });
1855
+ const hasHooks = profile.existingSettings?.hooks;
1856
+ checks.push({
1857
+ name: "Hooks",
1858
+ weight: 1,
1859
+ status: hasHooks ? "pass" : "warn",
1860
+ message: hasHooks ? "Hooks configured" : "No hooks in settings.json"
1861
+ });
1862
+ const perms = profile.existingSettings?.permissions;
1863
+ const denyList = perms?.deny || [];
1864
+ const envProtected = denyList.some((d) => d.includes(".env"));
1865
+ checks.push({
1866
+ name: ".env protection",
1867
+ weight: 2,
1868
+ status: envProtected ? "pass" : "warn",
1869
+ message: envProtected ? ".env in deny list" : ".env not in deny list"
1870
+ });
1871
+ if (profile.existingClaudeMd) {
1872
+ const requiredSections = ["## Purpose", "## Commands", "## Tech Stack"];
1873
+ const missingSections = requiredSections.filter(
1874
+ (s) => !profile.existingClaudeMd.includes(s)
1875
+ );
1876
+ if (missingSections.length > 0) {
1877
+ checks.push({
1878
+ name: "CLAUDE.md sections",
1879
+ weight: 1,
1880
+ status: "warn",
1881
+ message: `Missing: ${missingSections.join(", ")}`
1882
+ });
1883
+ } else {
1884
+ checks.push({
1885
+ name: "CLAUDE.md sections",
1886
+ weight: 1,
1887
+ status: "pass",
1888
+ message: "Required sections present"
1889
+ });
1890
+ }
1891
+ }
1892
+ return checks;
1893
+ }
1894
+ var doctorCommand = new Command7("doctor").description(
1895
+ "Validate the current Claude Code environment against best practices"
1896
+ ).action(async () => {
1897
+ const targetDir = process.cwd();
1898
+ console.log(chalk7.dim("\n Checking .claude/ environment...\n"));
1899
+ const profile = await scanProject(targetDir);
1900
+ if (!profile.hasClaudeDir) {
1901
+ console.log(chalk7.red(" \u274C No .claude/ directory found.\n"));
1902
+ console.log(
1903
+ chalk7.dim(" Run ") + chalk7.bold("kairn describe") + chalk7.dim(" or ") + chalk7.bold("kairn optimize") + chalk7.dim(" to generate one.\n")
1904
+ );
1905
+ process.exit(1);
1906
+ }
1907
+ const checks = runChecks(profile);
1908
+ for (const check of checks) {
1909
+ const icon = check.status === "pass" ? chalk7.green("\u2705") : check.status === "warn" ? chalk7.yellow("\u26A0\uFE0F ") : chalk7.red("\u274C");
1910
+ const msg = check.status === "pass" ? chalk7.dim(check.message) : check.status === "warn" ? chalk7.yellow(check.message) : chalk7.red(check.message);
1911
+ console.log(` ${icon} ${check.name}: ${msg}`);
1912
+ }
1913
+ const maxScore = checks.reduce((sum, c) => sum + c.weight, 0);
1914
+ const score = checks.reduce((sum, c) => {
1915
+ if (c.status === "pass") return sum + c.weight;
1916
+ if (c.status === "warn") return sum + Math.floor(c.weight / 2);
1917
+ return sum;
1918
+ }, 0);
1919
+ const percentage = Math.round(score / maxScore * 100);
1920
+ const scoreColor = percentage >= 80 ? chalk7.green : percentage >= 50 ? chalk7.yellow : chalk7.red;
1921
+ console.log(
1922
+ `
1923
+ Score: ${scoreColor(`${score}/${maxScore}`)} (${scoreColor(`${percentage}%`)})
1924
+ `
1925
+ );
1926
+ if (percentage < 80) {
1927
+ console.log(
1928
+ chalk7.dim(" Run ") + chalk7.bold("kairn optimize") + chalk7.dim(" to fix issues.\n")
1929
+ );
1930
+ }
1931
+ });
1932
+
1933
+ // src/commands/registry.ts
1934
+ import { Command as Command8 } from "commander";
1935
+ import chalk8 from "chalk";
1936
+ import { input as input2, select as select2 } from "@inquirer/prompts";
1937
+ var listCommand2 = new Command8("list").description("List tools in the registry").option("--category <cat>", "Filter by category").option("--user-only", "Show only user-defined tools").action(async (options) => {
1938
+ let all;
1939
+ let userTools;
1940
+ try {
1941
+ [all, userTools] = await Promise.all([loadRegistry(), loadUserRegistry()]);
1942
+ } catch (err) {
1943
+ const msg = err instanceof Error ? err.message : String(err);
1944
+ console.log(chalk8.red(`
1945
+ Failed to load registry: ${msg}
1946
+ `));
1947
+ process.exit(1);
1948
+ }
1949
+ const userIds = new Set(userTools.map((t) => t.id));
1950
+ let tools = all;
1951
+ if (options.userOnly) {
1952
+ tools = tools.filter((t) => userIds.has(t.id));
1953
+ }
1954
+ if (options.category) {
1955
+ tools = tools.filter(
1956
+ (t) => t.category.toLowerCase() === options.category.toLowerCase()
1957
+ );
1958
+ }
1959
+ if (tools.length === 0) {
1960
+ console.log(chalk8.dim("\n No tools found.\n"));
1961
+ return;
1962
+ }
1963
+ const bundledCount = all.filter((t) => !userIds.has(t.id)).length;
1964
+ const userCount = userIds.size;
1965
+ console.log(chalk8.cyan("\n Registry Tools\n"));
1966
+ for (const tool of tools) {
1967
+ const isUser = userIds.has(tool.id);
1968
+ const meta = [
1969
+ tool.category,
1970
+ `tier ${tool.tier}`,
1971
+ tool.auth
1972
+ ].join(", ");
1973
+ console.log(chalk8.bold(` ${tool.id}`) + chalk8.dim(` (${meta})`));
1974
+ console.log(chalk8.dim(` ${tool.description}`));
1975
+ if (tool.best_for.length > 0) {
1976
+ console.log(chalk8.dim(` Best for: ${tool.best_for.join(", ")}`));
1977
+ }
1978
+ if (isUser) {
1979
+ console.log(chalk8.yellow(" [USER-DEFINED]"));
1980
+ }
1981
+ console.log("");
1982
+ }
1983
+ const totalShown = tools.length;
1984
+ const shownUser = tools.filter((t) => userIds.has(t.id)).length;
1985
+ const shownBundled = totalShown - shownUser;
1986
+ console.log(
1987
+ chalk8.dim(
1988
+ ` ${totalShown} tool${totalShown !== 1 ? "s" : ""} (${shownBundled} bundled, ${shownUser} user-defined)`
1989
+ ) + "\n"
1990
+ );
1991
+ });
1992
+ var addCommand = new Command8("add").description("Add a tool to the user registry").action(async () => {
1993
+ let id;
1994
+ try {
1995
+ id = await input2({
1996
+ message: "Tool ID (kebab-case)",
1997
+ validate: (v) => {
1998
+ if (!v) return "ID is required";
1999
+ if (!/^[a-z][a-z0-9-]*$/.test(v)) return "ID must be kebab-case (e.g. my-tool)";
2000
+ return true;
2001
+ }
2002
+ });
2003
+ const name = await input2({ message: "Display name" });
2004
+ const description = await input2({ message: "Description" });
2005
+ const category = await select2({
2006
+ message: "Category",
2007
+ choices: [
2008
+ { value: "universal" },
2009
+ { value: "code" },
2010
+ { value: "search" },
2011
+ { value: "data" },
2012
+ { value: "communication" },
2013
+ { value: "design" },
2014
+ { value: "monitoring" },
2015
+ { value: "infrastructure" },
2016
+ { value: "sandbox" }
2017
+ ]
2018
+ });
2019
+ const tier = await select2({
2020
+ message: "Tier",
2021
+ choices: [
2022
+ { name: "1 \u2014 Universal", value: 1 },
2023
+ { name: "2 \u2014 Common", value: 2 },
2024
+ { name: "3 \u2014 Specialized", value: 3 }
2025
+ ]
2026
+ });
2027
+ const type = await select2({
2028
+ message: "Type",
2029
+ choices: [
2030
+ { value: "mcp_server" },
2031
+ { value: "plugin" },
2032
+ { value: "hook" }
2033
+ ]
2034
+ });
2035
+ const auth = await select2({
2036
+ message: "Auth",
2037
+ choices: [
2038
+ { value: "none" },
2039
+ { value: "api_key" },
2040
+ { value: "oauth" },
2041
+ { value: "connection_string" }
2042
+ ]
2043
+ });
2044
+ const env_vars = [];
2045
+ if (auth === "api_key" || auth === "connection_string") {
2046
+ let addMore = true;
2047
+ while (addMore) {
2048
+ const varName = await input2({ message: "Env var name" });
2049
+ const varDesc = await input2({ message: "Env var description" });
2050
+ env_vars.push({ name: varName, description: varDesc });
2051
+ const another = await select2({
2052
+ message: "Add another env var?",
2053
+ choices: [
2054
+ { name: "No", value: false },
2055
+ { name: "Yes", value: true }
2056
+ ]
2057
+ });
2058
+ addMore = another;
2059
+ }
2060
+ }
2061
+ const signup_url_raw = await input2({ message: "Signup URL (optional, press enter to skip)" });
2062
+ const signup_url = signup_url_raw.trim() || void 0;
2063
+ const best_for_raw = await input2({ message: "Best-for tags, comma-separated" });
2064
+ const best_for = best_for_raw.split(",").map((s) => s.trim()).filter(Boolean);
2065
+ const install = {};
2066
+ if (type === "mcp_server") {
2067
+ const command = await input2({ message: "MCP command" });
2068
+ const args_raw = await input2({ message: "MCP args, comma-separated (leave blank for none)" });
2069
+ const args = args_raw.split(",").map((s) => s.trim()).filter(Boolean);
2070
+ install.mcp_config = { command, args };
2071
+ }
2072
+ const tool = {
2073
+ id,
2074
+ name,
2075
+ description,
2076
+ category,
2077
+ tier,
2078
+ type,
2079
+ auth,
2080
+ best_for,
2081
+ install,
2082
+ ...env_vars.length > 0 ? { env_vars } : {},
2083
+ ...signup_url ? { signup_url } : {}
2084
+ };
2085
+ let userTools;
2086
+ try {
2087
+ userTools = await loadUserRegistry();
2088
+ } catch {
2089
+ userTools = [];
1371
2090
  }
2091
+ const existingIdx = userTools.findIndex((t) => t.id === id);
2092
+ if (existingIdx >= 0) {
2093
+ userTools[existingIdx] = tool;
2094
+ } else {
2095
+ userTools.push(tool);
2096
+ }
2097
+ await saveUserRegistry(userTools);
2098
+ console.log(chalk8.green(`
2099
+ \u2713 Tool ${id} added to user registry
2100
+ `));
2101
+ } catch (err) {
2102
+ const msg = err instanceof Error ? err.message : String(err);
2103
+ console.log(chalk8.red(`
2104
+ Failed to add tool: ${msg}
2105
+ `));
2106
+ process.exit(1);
2107
+ }
2108
+ });
2109
+ var registryCommand = new Command8("registry").description("Manage the tool registry").addCommand(listCommand2).addCommand(addCommand);
2110
+
2111
+ // src/commands/templates.ts
2112
+ import { Command as Command9 } from "commander";
2113
+ import chalk9 from "chalk";
2114
+ import fs12 from "fs/promises";
2115
+ import path12 from "path";
2116
+ var templatesCommand = new Command9("templates").description("Browse available templates").option("--category <cat>", "filter templates by category keyword").option("--json", "output raw JSON array").action(async (options) => {
2117
+ const templatesDir = getTemplatesDir();
2118
+ let files;
2119
+ try {
2120
+ files = await fs12.readdir(templatesDir);
2121
+ } catch {
2122
+ console.log(
2123
+ chalk9.dim(
2124
+ "\n No templates found. Templates will be installed with "
2125
+ ) + chalk9.bold("kairn init") + chalk9.dim(
2126
+ " or you can add .json files to ~/.kairn/templates/\n"
2127
+ )
2128
+ );
2129
+ return;
2130
+ }
2131
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
2132
+ if (jsonFiles.length === 0) {
2133
+ console.log(
2134
+ chalk9.dim(
2135
+ "\n No templates found. Templates will be installed with "
2136
+ ) + chalk9.bold("kairn init") + chalk9.dim(
2137
+ " or you can add .json files to ~/.kairn/templates/\n"
2138
+ )
2139
+ );
2140
+ return;
2141
+ }
2142
+ const templates = [];
2143
+ for (const file of jsonFiles) {
2144
+ try {
2145
+ const data = await fs12.readFile(
2146
+ path12.join(templatesDir, file),
2147
+ "utf-8"
2148
+ );
2149
+ const spec = JSON.parse(data);
2150
+ templates.push(spec);
2151
+ } catch {
2152
+ }
2153
+ }
2154
+ const filtered = options.category ? templates.filter((t) => {
2155
+ const keyword = options.category.toLowerCase();
2156
+ return t.intent?.toLowerCase().includes(keyword) || t.description?.toLowerCase().includes(keyword);
2157
+ }) : templates;
2158
+ if (options.json) {
2159
+ console.log(JSON.stringify(filtered, null, 2));
2160
+ return;
2161
+ }
2162
+ if (filtered.length === 0) {
2163
+ console.log(
2164
+ chalk9.dim(`
2165
+ No templates matched category "${options.category}".
2166
+ `)
2167
+ );
2168
+ return;
2169
+ }
2170
+ console.log(chalk9.cyan("\n Available Templates\n"));
2171
+ for (const spec of filtered) {
2172
+ const toolCount = spec.tools?.length ?? 0;
2173
+ const commandCount = Object.keys(spec.harness?.commands ?? {}).length;
2174
+ const ruleCount = Object.keys(spec.harness?.rules ?? {}).length;
2175
+ console.log(
2176
+ chalk9.bold(` ${spec.name}`) + chalk9.dim(` (ID: ${spec.id})`)
2177
+ );
2178
+ console.log(chalk9.dim(` ${spec.description}`));
2179
+ console.log(
2180
+ chalk9.dim(
2181
+ ` Tools: ${toolCount} | Commands: ${commandCount} | Rules: ${ruleCount}`
2182
+ )
2183
+ );
2184
+ console.log("");
1372
2185
  }
1373
2186
  console.log(
1374
- chalk6.cyan("\n Ready! Run ") + chalk6.bold("claude") + chalk6.cyan(" to start.\n")
2187
+ chalk9.dim(` ${filtered.length} template${filtered.length === 1 ? "" : "s"} available
2188
+ `)
1375
2189
  );
1376
2190
  });
1377
2191
 
1378
2192
  // src/cli.ts
1379
- var program = new Command7();
2193
+ var program = new Command10();
1380
2194
  program.name("kairn").description(
1381
2195
  "Compile natural language intent into optimized Claude Code environments"
1382
- ).version("1.3.0");
2196
+ ).version("1.5.0");
1383
2197
  program.addCommand(initCommand);
1384
2198
  program.addCommand(describeCommand);
1385
2199
  program.addCommand(optimizeCommand);
1386
2200
  program.addCommand(listCommand);
1387
2201
  program.addCommand(activateCommand);
1388
2202
  program.addCommand(updateRegistryCommand);
2203
+ program.addCommand(doctorCommand);
2204
+ program.addCommand(registryCommand);
2205
+ program.addCommand(templatesCommand);
1389
2206
  program.parse();
1390
2207
  //# sourceMappingURL=cli.js.map