kairn-cli 1.4.0 → 1.5.1

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 Command8 } 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";
@@ -424,6 +470,15 @@ Each MCP server costs 500-2000 tokens of context window.
424
470
  - \`/project:edit\` command (review and improve writing)
425
471
  - A writing-workflow skill
426
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
+
427
482
  ## Output Schema
428
483
 
429
484
  Return ONLY valid JSON matching this structure:
@@ -477,18 +532,21 @@ Return ONLY valid JSON matching this structure:
477
532
 
478
533
  Do not include any text outside the JSON object. Do not wrap in markdown code fences.`;
479
534
 
480
- // src/compiler/compile.ts
481
- async function loadRegistry() {
482
- const __filename = fileURLToPath(import.meta.url);
483
- 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() {
484
542
  const candidates = [
485
- path2.resolve(__dirname, "../registry/tools.json"),
486
- path2.resolve(__dirname, "../src/registry/tools.json"),
487
- 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")
488
546
  ];
489
547
  for (const candidate of candidates) {
490
548
  try {
491
- const data = await fs2.readFile(candidate, "utf-8");
549
+ const data = await fs3.readFile(candidate, "utf-8");
492
550
  return JSON.parse(data);
493
551
  } catch {
494
552
  continue;
@@ -496,6 +554,32 @@ async function loadRegistry() {
496
554
  }
497
555
  throw new Error("Could not find tools.json registry");
498
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
499
583
  function buildUserMessage(intent, registry) {
500
584
  const registrySummary = registry.map(
501
585
  (t) => `- ${t.id} (${t.type}, tier ${t.tier}, auth: ${t.auth}): ${t.description} [best_for: ${t.best_for.join(", ")}]`
@@ -643,17 +727,17 @@ async function compile(intent, onProgress) {
643
727
  };
644
728
  validateSpec(spec, onProgress);
645
729
  await ensureDirs();
646
- const envPath = path2.join(getEnvsDir(), `${spec.id}.json`);
647
- 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");
648
732
  return spec;
649
733
  }
650
734
 
651
735
  // src/adapter/claude-code.ts
652
- import fs3 from "fs/promises";
653
- import path3 from "path";
736
+ import fs5 from "fs/promises";
737
+ import path5 from "path";
654
738
  async function writeFile(filePath, content) {
655
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
656
- await fs3.writeFile(filePath, content, "utf-8");
739
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
740
+ await fs5.writeFile(filePath, content, "utf-8");
657
741
  }
658
742
  function buildFileMap(spec) {
659
743
  const files = /* @__PURE__ */ new Map();
@@ -700,55 +784,55 @@ function buildFileMap(spec) {
700
784
  return files;
701
785
  }
702
786
  async function writeEnvironment(spec, targetDir) {
703
- const claudeDir = path3.join(targetDir, ".claude");
787
+ const claudeDir = path5.join(targetDir, ".claude");
704
788
  const written = [];
705
789
  if (spec.harness.claude_md) {
706
- const p = path3.join(claudeDir, "CLAUDE.md");
790
+ const p = path5.join(claudeDir, "CLAUDE.md");
707
791
  await writeFile(p, spec.harness.claude_md);
708
792
  written.push(".claude/CLAUDE.md");
709
793
  }
710
794
  if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
711
- const p = path3.join(claudeDir, "settings.json");
795
+ const p = path5.join(claudeDir, "settings.json");
712
796
  await writeFile(p, JSON.stringify(spec.harness.settings, null, 2));
713
797
  written.push(".claude/settings.json");
714
798
  }
715
799
  if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
716
- const p = path3.join(targetDir, ".mcp.json");
800
+ const p = path5.join(targetDir, ".mcp.json");
717
801
  const mcpContent = { mcpServers: spec.harness.mcp_config };
718
802
  await writeFile(p, JSON.stringify(mcpContent, null, 2));
719
803
  written.push(".mcp.json");
720
804
  }
721
805
  if (spec.harness.commands) {
722
806
  for (const [name, content] of Object.entries(spec.harness.commands)) {
723
- const p = path3.join(claudeDir, "commands", `${name}.md`);
807
+ const p = path5.join(claudeDir, "commands", `${name}.md`);
724
808
  await writeFile(p, content);
725
809
  written.push(`.claude/commands/${name}.md`);
726
810
  }
727
811
  }
728
812
  if (spec.harness.rules) {
729
813
  for (const [name, content] of Object.entries(spec.harness.rules)) {
730
- const p = path3.join(claudeDir, "rules", `${name}.md`);
814
+ const p = path5.join(claudeDir, "rules", `${name}.md`);
731
815
  await writeFile(p, content);
732
816
  written.push(`.claude/rules/${name}.md`);
733
817
  }
734
818
  }
735
819
  if (spec.harness.skills) {
736
820
  for (const [skillPath, content] of Object.entries(spec.harness.skills)) {
737
- const p = path3.join(claudeDir, "skills", `${skillPath}.md`);
821
+ const p = path5.join(claudeDir, "skills", `${skillPath}.md`);
738
822
  await writeFile(p, content);
739
823
  written.push(`.claude/skills/${skillPath}.md`);
740
824
  }
741
825
  }
742
826
  if (spec.harness.agents) {
743
827
  for (const [name, content] of Object.entries(spec.harness.agents)) {
744
- const p = path3.join(claudeDir, "agents", `${name}.md`);
828
+ const p = path5.join(claudeDir, "agents", `${name}.md`);
745
829
  await writeFile(p, content);
746
830
  written.push(`.claude/agents/${name}.md`);
747
831
  }
748
832
  }
749
833
  if (spec.harness.docs) {
750
834
  for (const [name, content] of Object.entries(spec.harness.docs)) {
751
- const p = path3.join(claudeDir, "docs", `${name}.md`);
835
+ const p = path5.join(claudeDir, "docs", `${name}.md`);
752
836
  await writeFile(p, content);
753
837
  written.push(`.claude/docs/${name}.md`);
754
838
  }
@@ -786,27 +870,131 @@ function summarizeSpec(spec, registry) {
786
870
  };
787
871
  }
788
872
 
789
- // src/commands/describe.ts
790
- import { fileURLToPath as fileURLToPath2 } from "url";
791
- async function loadRegistry2() {
792
- const __filename = fileURLToPath2(import.meta.url);
793
- const __dirname = path4.dirname(__filename);
794
- const candidates = [
795
- path4.resolve(__dirname, "../registry/tools.json"),
796
- path4.resolve(__dirname, "../src/registry/tools.json"),
797
- path4.resolve(__dirname, "../../src/registry/tools.json")
798
- ];
799
- for (const candidate of candidates) {
800
- try {
801
- const data = await fs4.readFile(candidate, "utf-8");
802
- return JSON.parse(data);
803
- } catch {
804
- 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 "[]";
805
899
  }
900
+ return obj.map((item) => `${pad}- ${toYaml(item, indent + 1).trimStart()}`).join("\n");
806
901
  }
807
- 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);
925
+ }
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;
808
994
  }
809
- 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) => {
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) => {
810
998
  const config = await loadConfig();
811
999
  if (!config) {
812
1000
  console.log(
@@ -836,7 +1024,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
836
1024
  `));
837
1025
  process.exit(1);
838
1026
  }
839
- const registry = await loadRegistry2();
1027
+ const registry = await loadRegistry();
840
1028
  const summary = summarizeSpec(spec, registry);
841
1029
  console.log(chalk2.green("\n \u2713 Environment compiled\n"));
842
1030
  console.log(chalk2.cyan(" Name: ") + spec.name);
@@ -870,46 +1058,53 @@ var describeCommand = new Command2("describe").description("Describe your workfl
870
1058
  return;
871
1059
  }
872
1060
  const targetDir = process.cwd();
873
- const written = await writeEnvironment(spec, targetDir);
874
- console.log(chalk2.green("\n \u2713 Environment written\n"));
875
- for (const file of written) {
876
- console.log(chalk2.dim(` ${file}`));
877
- }
878
- if (summary.envSetup.length > 0) {
879
- console.log(chalk2.yellow("\n API keys needed (set these environment variables):\n"));
880
- const seen = /* @__PURE__ */ new Set();
881
- for (const env of summary.envSetup) {
882
- if (seen.has(env.envVar)) continue;
883
- seen.add(env.envVar);
884
- console.log(chalk2.bold(` export ${env.envVar}="your-key-here"`));
885
- console.log(chalk2.dim(` ${env.description}`));
886
- if (env.signupUrl) {
887
- 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("");
888
1084
  }
889
- console.log("");
890
1085
  }
891
- }
892
- if (summary.pluginCommands.length > 0) {
893
- console.log(chalk2.yellow(" Install plugins by running these in Claude Code:"));
894
- for (const cmd of summary.pluginCommands) {
895
- 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
+ }
896
1091
  }
1092
+ console.log(
1093
+ chalk2.cyan("\n Ready! Run ") + chalk2.bold("claude") + chalk2.cyan(" to start.\n")
1094
+ );
897
1095
  }
898
- console.log(
899
- chalk2.cyan("\n Ready! Run ") + chalk2.bold("claude") + chalk2.cyan(" to start.\n")
900
- );
901
1096
  });
902
1097
 
903
1098
  // src/commands/list.ts
904
1099
  import { Command as Command3 } from "commander";
905
1100
  import chalk3 from "chalk";
906
- import fs5 from "fs/promises";
907
- import path5 from "path";
1101
+ import fs7 from "fs/promises";
1102
+ import path7 from "path";
908
1103
  var listCommand = new Command3("list").description("Show saved environments").action(async () => {
909
1104
  const envsDir = getEnvsDir();
910
1105
  let files;
911
1106
  try {
912
- files = await fs5.readdir(envsDir);
1107
+ files = await fs7.readdir(envsDir);
913
1108
  } catch {
914
1109
  console.log(chalk3.dim("\n No environments yet. Run ") + chalk3.bold("kairn describe") + chalk3.dim(" to create one.\n"));
915
1110
  return;
@@ -922,7 +1117,7 @@ var listCommand = new Command3("list").description("Show saved environments").ac
922
1117
  console.log(chalk3.cyan("\n Saved Environments\n"));
923
1118
  for (const file of jsonFiles) {
924
1119
  try {
925
- const data = await fs5.readFile(path5.join(envsDir, file), "utf-8");
1120
+ const data = await fs7.readFile(path7.join(envsDir, file), "utf-8");
926
1121
  const spec = JSON.parse(data);
927
1122
  const date = new Date(spec.created_at).toLocaleDateString();
928
1123
  const toolCount = spec.tools?.length ?? 0;
@@ -940,30 +1135,49 @@ var listCommand = new Command3("list").description("Show saved environments").ac
940
1135
  // src/commands/activate.ts
941
1136
  import { Command as Command4 } from "commander";
942
1137
  import chalk4 from "chalk";
943
- import fs6 from "fs/promises";
944
- import path6 from "path";
1138
+ import fs8 from "fs/promises";
1139
+ import path8 from "path";
945
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) => {
946
1141
  const envsDir = getEnvsDir();
947
- let files;
1142
+ const templatesDir = getTemplatesDir();
1143
+ let sourceDir;
1144
+ let match;
1145
+ let fromTemplate = false;
1146
+ let envFiles = [];
948
1147
  try {
949
- files = await fs6.readdir(envsDir);
1148
+ envFiles = await fs8.readdir(envsDir);
950
1149
  } catch {
951
- console.log(chalk4.red("\n No saved environments found.\n"));
952
- process.exit(1);
953
1150
  }
954
- const match = files.find(
1151
+ match = envFiles.find(
955
1152
  (f) => f === `${envId}.json` || f.startsWith(envId)
956
1153
  );
957
- if (!match) {
958
- 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(`
959
1170
  Environment "${envId}" not found.`));
960
- console.log(chalk4.dim(" Run kairn list to see saved environments.\n"));
961
- 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
+ }
962
1175
  }
963
- const data = await fs6.readFile(path6.join(envsDir, match), "utf-8");
1176
+ const data = await fs8.readFile(path8.join(sourceDir, match), "utf-8");
964
1177
  const spec = JSON.parse(data);
1178
+ const label = fromTemplate ? chalk4.dim(" (template)") : "";
965
1179
  console.log(chalk4.cyan(`
966
- Activating: ${spec.name}`));
1180
+ Activating: ${spec.name}`) + label);
967
1181
  console.log(chalk4.dim(` ${spec.description}
968
1182
  `));
969
1183
  const targetDir = process.cwd();
@@ -980,21 +1194,21 @@ var activateCommand = new Command4("activate").description("Re-deploy a saved en
980
1194
  // src/commands/update-registry.ts
981
1195
  import { Command as Command5 } from "commander";
982
1196
  import chalk5 from "chalk";
983
- import fs7 from "fs/promises";
984
- import path7 from "path";
1197
+ import fs9 from "fs/promises";
1198
+ import path9 from "path";
985
1199
  import { fileURLToPath as fileURLToPath3 } from "url";
986
1200
  var REGISTRY_URL = "https://raw.githubusercontent.com/ashtonperlroth/kairn/main/src/registry/tools.json";
987
1201
  async function getLocalRegistryPath() {
988
- const __filename = fileURLToPath3(import.meta.url);
989
- const __dirname = path7.dirname(__filename);
1202
+ const __filename3 = fileURLToPath3(import.meta.url);
1203
+ const __dirname3 = path9.dirname(__filename3);
990
1204
  const candidates = [
991
- path7.resolve(__dirname, "../registry/tools.json"),
992
- path7.resolve(__dirname, "../src/registry/tools.json"),
993
- 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")
994
1208
  ];
995
1209
  for (const candidate of candidates) {
996
1210
  try {
997
- await fs7.access(candidate);
1211
+ await fs9.access(candidate);
998
1212
  return candidate;
999
1213
  } catch {
1000
1214
  continue;
@@ -1035,10 +1249,10 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
1035
1249
  const registryPath = await getLocalRegistryPath();
1036
1250
  const backupPath = registryPath + ".bak";
1037
1251
  try {
1038
- await fs7.copyFile(registryPath, backupPath);
1252
+ await fs9.copyFile(registryPath, backupPath);
1039
1253
  } catch {
1040
1254
  }
1041
- await fs7.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
1255
+ await fs9.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
1042
1256
  console.log(chalk5.green(` \u2713 Registry updated: ${tools.length} tools`));
1043
1257
  console.log(chalk5.dim(` Saved to: ${registryPath}`));
1044
1258
  console.log(chalk5.dim(` Backup: ${backupPath}
@@ -1054,16 +1268,15 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
1054
1268
  import { Command as Command6 } from "commander";
1055
1269
  import { confirm as confirm2 } from "@inquirer/prompts";
1056
1270
  import chalk6 from "chalk";
1057
- import fs9 from "fs/promises";
1058
- import path9 from "path";
1059
- import { fileURLToPath as fileURLToPath4 } from "url";
1271
+ import fs11 from "fs/promises";
1272
+ import path11 from "path";
1060
1273
 
1061
1274
  // src/scanner/scan.ts
1062
- import fs8 from "fs/promises";
1063
- import path8 from "path";
1275
+ import fs10 from "fs/promises";
1276
+ import path10 from "path";
1064
1277
  async function fileExists(p) {
1065
1278
  try {
1066
- await fs8.access(p);
1279
+ await fs10.access(p);
1067
1280
  return true;
1068
1281
  } catch {
1069
1282
  return false;
@@ -1071,7 +1284,7 @@ async function fileExists(p) {
1071
1284
  }
1072
1285
  async function readJsonSafe(p) {
1073
1286
  try {
1074
- const data = await fs8.readFile(p, "utf-8");
1287
+ const data = await fs10.readFile(p, "utf-8");
1075
1288
  return JSON.parse(data);
1076
1289
  } catch {
1077
1290
  return null;
@@ -1079,14 +1292,14 @@ async function readJsonSafe(p) {
1079
1292
  }
1080
1293
  async function readFileSafe(p) {
1081
1294
  try {
1082
- return await fs8.readFile(p, "utf-8");
1295
+ return await fs10.readFile(p, "utf-8");
1083
1296
  } catch {
1084
1297
  return null;
1085
1298
  }
1086
1299
  }
1087
1300
  async function listDirSafe(p) {
1088
1301
  try {
1089
- const entries = await fs8.readdir(p);
1302
+ const entries = await fs10.readdir(p);
1090
1303
  return entries.filter((e) => !e.startsWith("."));
1091
1304
  } catch {
1092
1305
  return [];
@@ -1138,7 +1351,7 @@ function extractEnvKeys(content) {
1138
1351
  return keys;
1139
1352
  }
1140
1353
  async function scanProject(dir) {
1141
- const pkg = await readJsonSafe(path8.join(dir, "package.json"));
1354
+ const pkg = await readJsonSafe(path10.join(dir, "package.json"));
1142
1355
  const deps = pkg?.dependencies ? Object.keys(pkg.dependencies) : [];
1143
1356
  const devDeps = pkg?.devDependencies ? Object.keys(pkg.devDependencies) : [];
1144
1357
  const allDeps = [...deps, ...devDeps];
@@ -1166,19 +1379,19 @@ async function scanProject(dir) {
1166
1379
  const framework = detectFramework(allDeps);
1167
1380
  const typescript = keyFiles.includes("tsconfig.json") || allDeps.includes("typescript");
1168
1381
  const testCommand = scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1' ? scripts.test : null;
1169
- 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"));
1170
1383
  const buildCommand = scripts.build || null;
1171
1384
  const lintCommand = scripts.lint || null;
1172
- const hasSrc = await fileExists(path8.join(dir, "src"));
1173
- const hasDocker = await fileExists(path8.join(dir, "docker-compose.yml")) || await fileExists(path8.join(dir, "Dockerfile"));
1174
- const hasCi = await fileExists(path8.join(dir, ".github/workflows"));
1175
- 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"));
1176
1389
  let envKeys = [];
1177
- const envExample = await readFileSafe(path8.join(dir, ".env.example"));
1390
+ const envExample = await readFileSafe(path10.join(dir, ".env.example"));
1178
1391
  if (envExample) {
1179
1392
  envKeys = extractEnvKeys(envExample);
1180
1393
  }
1181
- const claudeDir = path8.join(dir, ".claude");
1394
+ const claudeDir = path10.join(dir, ".claude");
1182
1395
  const hasClaudeDir = await fileExists(claudeDir);
1183
1396
  let existingClaudeMd = null;
1184
1397
  let existingSettings = null;
@@ -1190,21 +1403,21 @@ async function scanProject(dir) {
1190
1403
  let mcpServerCount = 0;
1191
1404
  let claudeMdLineCount = 0;
1192
1405
  if (hasClaudeDir) {
1193
- existingClaudeMd = await readFileSafe(path8.join(claudeDir, "CLAUDE.md"));
1406
+ existingClaudeMd = await readFileSafe(path10.join(claudeDir, "CLAUDE.md"));
1194
1407
  if (existingClaudeMd) {
1195
1408
  claudeMdLineCount = existingClaudeMd.split("\n").length;
1196
1409
  }
1197
- existingSettings = await readJsonSafe(path8.join(claudeDir, "settings.json"));
1198
- 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"));
1199
1412
  if (existingMcpConfig?.mcpServers) {
1200
1413
  mcpServerCount = Object.keys(existingMcpConfig.mcpServers).length;
1201
1414
  }
1202
- existingCommands = (await listDirSafe(path8.join(claudeDir, "commands"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1203
- existingRules = (await listDirSafe(path8.join(claudeDir, "rules"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1204
- existingSkills = await listDirSafe(path8.join(claudeDir, "skills"));
1205
- 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", ""));
1206
1419
  }
1207
- const name = pkg?.name || path8.basename(dir);
1420
+ const name = pkg?.name || path10.basename(dir);
1208
1421
  const description = pkg?.description || "";
1209
1422
  return {
1210
1423
  name,
@@ -1263,10 +1476,10 @@ async function generateDiff(spec, targetDir) {
1263
1476
  const fileMap = buildFileMap(spec);
1264
1477
  const results = [];
1265
1478
  for (const [relativePath, newContent] of fileMap) {
1266
- const absolutePath = path9.join(targetDir, relativePath);
1479
+ const absolutePath = path11.join(targetDir, relativePath);
1267
1480
  let oldContent = null;
1268
1481
  try {
1269
- oldContent = await fs9.readFile(absolutePath, "utf-8");
1482
+ oldContent = await fs11.readFile(absolutePath, "utf-8");
1270
1483
  } catch {
1271
1484
  }
1272
1485
  if (oldContent === null) {
@@ -1292,24 +1505,6 @@ async function generateDiff(spec, targetDir) {
1292
1505
  }
1293
1506
  return results;
1294
1507
  }
1295
- async function loadRegistry3() {
1296
- const __filename = fileURLToPath4(import.meta.url);
1297
- const __dirname = path9.dirname(__filename);
1298
- const candidates = [
1299
- path9.resolve(__dirname, "../registry/tools.json"),
1300
- path9.resolve(__dirname, "../src/registry/tools.json"),
1301
- path9.resolve(__dirname, "../../src/registry/tools.json")
1302
- ];
1303
- for (const candidate of candidates) {
1304
- try {
1305
- const data = await fs9.readFile(candidate, "utf-8");
1306
- return JSON.parse(data);
1307
- } catch {
1308
- continue;
1309
- }
1310
- }
1311
- throw new Error("Could not find tools.json registry");
1312
- }
1313
1508
  function buildProfileSummary(profile) {
1314
1509
  const lines = [];
1315
1510
  lines.push(`Project: ${profile.name}`);
@@ -1388,7 +1583,7 @@ ${profile.existingClaudeMd}`);
1388
1583
  }
1389
1584
  return parts.join("\n");
1390
1585
  }
1391
- 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").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) => {
1392
1587
  const config = await loadConfig();
1393
1588
  if (!config) {
1394
1589
  console.log(
@@ -1479,7 +1674,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1479
1674
  `));
1480
1675
  process.exit(1);
1481
1676
  }
1482
- const registry = await loadRegistry3();
1677
+ const registry = await loadRegistry();
1483
1678
  const summary = summarizeSpec(spec, registry);
1484
1679
  console.log(chalk6.green(" \u2713 Environment compiled\n"));
1485
1680
  console.log(chalk6.cyan(" Name: ") + spec.name);
@@ -1530,34 +1725,41 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1530
1725
  return;
1531
1726
  }
1532
1727
  }
1533
- const written = await writeEnvironment(spec, targetDir);
1534
- console.log(chalk6.green("\n \u2713 Environment written\n"));
1535
- for (const file of written) {
1536
- console.log(chalk6.dim(` ${file}`));
1537
- }
1538
- if (summary.envSetup.length > 0) {
1539
- console.log(chalk6.yellow("\n API keys needed (set these environment variables):\n"));
1540
- const seen = /* @__PURE__ */ new Set();
1541
- for (const env of summary.envSetup) {
1542
- if (seen.has(env.envVar)) continue;
1543
- seen.add(env.envVar);
1544
- console.log(chalk6.bold(` export ${env.envVar}="your-key-here"`));
1545
- console.log(chalk6.dim(` ${env.description}`));
1546
- if (env.signupUrl) {
1547
- console.log(chalk6.dim(` Get one at: ${env.signupUrl}`));
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("");
1548
1751
  }
1549
- console.log("");
1550
1752
  }
1551
- }
1552
- if (summary.pluginCommands.length > 0) {
1553
- console.log(chalk6.yellow(" Install plugins by running these in Claude Code:"));
1554
- for (const cmd of summary.pluginCommands) {
1555
- console.log(chalk6.bold(` ${cmd}`));
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
+ }
1556
1758
  }
1759
+ console.log(
1760
+ chalk6.cyan("\n Ready! Run ") + chalk6.bold("claude") + chalk6.cyan(" to start.\n")
1761
+ );
1557
1762
  }
1558
- console.log(
1559
- chalk6.cyan("\n Ready! Run ") + chalk6.bold("claude") + chalk6.cyan(" to start.\n")
1560
- );
1561
1763
  });
1562
1764
 
1563
1765
  // src/commands/doctor.ts
@@ -1728,11 +1930,270 @@ var doctorCommand = new Command7("doctor").description(
1728
1930
  }
1729
1931
  });
1730
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 = [];
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("");
2185
+ }
2186
+ console.log(
2187
+ chalk9.dim(` ${filtered.length} template${filtered.length === 1 ? "" : "s"} available
2188
+ `)
2189
+ );
2190
+ });
2191
+
1731
2192
  // src/cli.ts
1732
- var program = new Command8();
2193
+ var program = new Command10();
1733
2194
  program.name("kairn").description(
1734
2195
  "Compile natural language intent into optimized Claude Code environments"
1735
- ).version("1.4.0");
2196
+ ).version("1.5.1");
1736
2197
  program.addCommand(initCommand);
1737
2198
  program.addCommand(describeCommand);
1738
2199
  program.addCommand(optimizeCommand);
@@ -1740,5 +2201,7 @@ program.addCommand(listCommand);
1740
2201
  program.addCommand(activateCommand);
1741
2202
  program.addCommand(updateRegistryCommand);
1742
2203
  program.addCommand(doctorCommand);
2204
+ program.addCommand(registryCommand);
2205
+ program.addCommand(templatesCommand);
1743
2206
  program.parse();
1744
2207
  //# sourceMappingURL=cli.js.map