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 +635 -172
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- package/src/registry/templates/api-service.json +92 -0
- package/src/registry/templates/content-writing.json +49 -0
- package/src/registry/templates/nextjs-fullstack.json +94 -0
- package/src/registry/templates/research-project.json +50 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { Command as
|
|
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
|
|
198
|
-
import
|
|
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/
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
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 =
|
|
647
|
-
await
|
|
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
|
|
653
|
-
import
|
|
736
|
+
import fs5 from "fs/promises";
|
|
737
|
+
import path5 from "path";
|
|
654
738
|
async function writeFile(filePath, content) {
|
|
655
|
-
await
|
|
656
|
-
await
|
|
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 =
|
|
787
|
+
const claudeDir = path5.join(targetDir, ".claude");
|
|
704
788
|
const written = [];
|
|
705
789
|
if (spec.harness.claude_md) {
|
|
706
|
-
const p =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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/
|
|
790
|
-
import
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
console.log(chalk2.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
for (const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
console.log(chalk2.
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
907
|
-
import
|
|
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
|
|
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
|
|
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
|
|
944
|
-
import
|
|
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
|
-
|
|
1142
|
+
const templatesDir = getTemplatesDir();
|
|
1143
|
+
let sourceDir;
|
|
1144
|
+
let match;
|
|
1145
|
+
let fromTemplate = false;
|
|
1146
|
+
let envFiles = [];
|
|
948
1147
|
try {
|
|
949
|
-
|
|
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
|
-
|
|
1151
|
+
match = envFiles.find(
|
|
955
1152
|
(f) => f === `${envId}.json` || f.startsWith(envId)
|
|
956
1153
|
);
|
|
957
|
-
if (
|
|
958
|
-
|
|
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
|
-
|
|
961
|
-
|
|
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
|
|
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
|
|
984
|
-
import
|
|
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
|
|
989
|
-
const
|
|
1202
|
+
const __filename3 = fileURLToPath3(import.meta.url);
|
|
1203
|
+
const __dirname3 = path9.dirname(__filename3);
|
|
990
1204
|
const candidates = [
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
|
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
|
|
1252
|
+
await fs9.copyFile(registryPath, backupPath);
|
|
1039
1253
|
} catch {
|
|
1040
1254
|
}
|
|
1041
|
-
await
|
|
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
|
|
1058
|
-
import
|
|
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
|
|
1063
|
-
import
|
|
1275
|
+
import fs10 from "fs/promises";
|
|
1276
|
+
import path10 from "path";
|
|
1064
1277
|
async function fileExists(p) {
|
|
1065
1278
|
try {
|
|
1066
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
1173
|
-
const hasDocker = await fileExists(
|
|
1174
|
-
const hasCi = await fileExists(
|
|
1175
|
-
const hasEnvFile = await fileExists(
|
|
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(
|
|
1390
|
+
const envExample = await readFileSafe(path10.join(dir, ".env.example"));
|
|
1178
1391
|
if (envExample) {
|
|
1179
1392
|
envKeys = extractEnvKeys(envExample);
|
|
1180
1393
|
}
|
|
1181
|
-
const claudeDir =
|
|
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(
|
|
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(
|
|
1198
|
-
existingMcpConfig = await readJsonSafe(
|
|
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(
|
|
1203
|
-
existingRules = (await listDirSafe(
|
|
1204
|
-
existingSkills = await listDirSafe(
|
|
1205
|
-
existingAgents = (await listDirSafe(
|
|
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 ||
|
|
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 =
|
|
1479
|
+
const absolutePath = path11.join(targetDir, relativePath);
|
|
1267
1480
|
let oldContent = null;
|
|
1268
1481
|
try {
|
|
1269
|
-
oldContent = await
|
|
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
|
|
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
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
console.log(chalk6.
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
for (const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
console.log(chalk6.
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
|
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.
|
|
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
|