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