kairn-cli 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/cli.ts
2
- import { Command as Command7 } from "commander";
2
+ import { Command as Command8 } from "commander";
3
3
 
4
4
  // src/commands/init.ts
5
5
  import { Command } from "commander";
@@ -217,6 +217,37 @@ You must output a JSON object matching the EnvironmentSpec schema.
217
217
  - **Concise CLAUDE.md.** Under 100 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
218
218
  - **Security by default.** Always include deny rules for destructive commands and secret file access.
219
219
 
220
+ ## CLAUDE.md Template (mandatory structure)
221
+
222
+ The \`claude_md\` field MUST follow this exact structure (max 100 lines):
223
+
224
+ \`\`\`
225
+ # {Project Name}
226
+
227
+ ## Purpose
228
+ {one-line description}
229
+
230
+ ## Tech Stack
231
+ {bullet list of frameworks/languages}
232
+
233
+ ## Commands
234
+ {concrete build/test/lint/dev commands}
235
+
236
+ ## Architecture
237
+ {brief folder structure, max 10 lines}
238
+
239
+ ## Conventions
240
+ {3-5 specific coding rules}
241
+
242
+ ## Key Commands
243
+ {list /project: commands with descriptions}
244
+
245
+ ## Output
246
+ {where results go, key files}
247
+ \`\`\`
248
+
249
+ Do not add generic filler. Every line must be specific to the user's workflow.
250
+
220
251
  ## What You Must Always Include
221
252
 
222
253
  1. A concise, workflow-specific \`claude_md\` (the CLAUDE.md content)
@@ -228,6 +259,116 @@ You must output a JSON object matching the EnvironmentSpec schema.
228
259
  7. A \`rules/continuity.md\` rule encouraging updates to DECISIONS.md and LEARNINGS.md
229
260
  8. A \`rules/security.md\` rule with essential security instructions
230
261
  9. settings.json with deny rules for \`rm -rf\`, \`curl|sh\`, reading \`.env\` and \`secrets/\`
262
+ 10. A \`/project:status\` command for code projects (uses ! for live git/test output)
263
+ 11. A \`/project:fix\` command for code projects (uses $ARGUMENTS for issue number)
264
+ 12. A \`docs/SPRINT.md\` file for sprint contracts (acceptance criteria, verification steps)
265
+
266
+ ## Shell-Integrated Commands
267
+
268
+ Commands that reference live project state should use Claude Code's \`!\` prefix for shell output:
269
+
270
+ \`\`\`markdown
271
+ # Example: .claude/commands/review.md
272
+ Review the staged changes for quality and security:
273
+
274
+ !git diff --staged
275
+
276
+ Run tests and check for failures:
277
+
278
+ !npm test 2>&1 | tail -20
279
+
280
+ Focus on: security, error handling, test coverage.
281
+ \`\`\`
282
+
283
+ Use \`!\` when a command needs: git status, test results, build output, or file listings.
284
+
285
+ ## Path-Scoped Rules
286
+
287
+ For code projects with multiple domains (API, frontend, tests), generate path-scoped rules using YAML frontmatter:
288
+
289
+ \`\`\`markdown
290
+ # Example: rules/api.md
291
+ ---
292
+ paths:
293
+ - "src/api/**"
294
+ - "src/routes/**"
295
+ ---
296
+ - All handlers return { data, error } shape
297
+ - Use Zod for request validation
298
+ - Log errors with request ID context
299
+ \`\`\`
300
+
301
+ \`\`\`markdown
302
+ # Example: rules/testing.md
303
+ ---
304
+ paths:
305
+ - "tests/**"
306
+ - "**/*.test.*"
307
+ - "**/*.spec.*"
308
+ ---
309
+ - Use AAA pattern: Arrange-Act-Assert
310
+ - One assertion per test when possible
311
+ - Mock external dependencies, never real APIs
312
+ \`\`\`
313
+
314
+ Keep \`security.md\` and \`continuity.md\` as unconditional (no paths frontmatter).
315
+ Only generate scoped rules when the workflow involves multiple code domains.
316
+
317
+ ## Hooks
318
+
319
+ Generate hooks in settings.json based on project type:
320
+
321
+ **All code projects** \u2014 block destructive commands:
322
+ \`\`\`json
323
+ {
324
+ "hooks": {
325
+ "PreToolUse": [{
326
+ "matcher": "Bash",
327
+ "hooks": [{
328
+ "type": "command",
329
+ "command": "CMD=$(cat | jq -r '.tool_input.command // empty') && echo \\"$CMD\\" | grep -qiE 'rm\\\\s+-rf\\\\s+/|DROP\\\\s+TABLE|curl.*\\\\|\\\\s*sh' && echo 'Blocked destructive command' >&2 && exit 2 || true"
330
+ }]
331
+ }]
332
+ }
333
+ }
334
+ \`\`\`
335
+
336
+ **Projects with Prettier/ESLint/Black** \u2014 auto-format on write:
337
+ \`\`\`json
338
+ {
339
+ "hooks": {
340
+ "PostToolUse": [{
341
+ "matcher": "Edit|Write",
342
+ "hooks": [{
343
+ "type": "command",
344
+ "command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \\"$FILE\\" ] && npx prettier --write \\"$FILE\\" 2>/dev/null || true"
345
+ }]
346
+ }]
347
+ }
348
+ }
349
+ \`\`\`
350
+
351
+ 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).
352
+
353
+ ## PostCompact Hook
354
+
355
+ All projects should include a PostCompact hook to restore context after compaction:
356
+
357
+ \`\`\`json
358
+ {
359
+ "hooks": {
360
+ "PostCompact": [{
361
+ "matcher": "",
362
+ "hooks": [{
363
+ "type": "prompt",
364
+ "prompt": "Re-read CLAUDE.md and docs/SPRINT.md (if it exists) to restore project context after compaction."
365
+ }]
366
+ }]
367
+ }
368
+ }
369
+ \`\`\`
370
+
371
+ Merge this into the settings hooks alongside the PreToolUse and PostToolUse hooks.
231
372
 
232
373
  ## Tool Selection Rules
233
374
 
@@ -238,14 +379,37 @@ You must output a JSON object matching the EnvironmentSpec schema.
238
379
  - Maximum 6-8 MCP servers to avoid context bloat
239
380
  - Include a \`reason\` for each selected tool explaining why it fits this workflow
240
381
 
382
+ ## Context Budget (STRICT)
383
+
384
+ - MCP servers: maximum 6. Prefer fewer.
385
+ - CLAUDE.md: maximum 100 lines.
386
+ - Rules: maximum 5 files, each under 20 lines.
387
+ - Skills: maximum 3. Only include directly relevant ones.
388
+ - Agents: maximum 3. QA pipeline + one specialist.
389
+ - Commands: no limit (loaded on demand, zero context cost).
390
+ - Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
391
+
392
+ If the workflow doesn't clearly need a tool, DO NOT include it.
393
+ Each MCP server costs 500-2000 tokens of context window.
394
+
241
395
  ## For Code Projects, Additionally Include
242
396
 
243
397
  - \`/project:plan\` command (plan before coding)
244
398
  - \`/project:review\` command (review changes)
245
399
  - \`/project:test\` command (run and fix tests)
246
400
  - \`/project:commit\` command (conventional commits)
247
- - A TDD skill if testing is relevant
248
- - A reviewer agent (read-only, Sonnet model)
401
+ - \`/project:status\` command (live git status, recent commits, TODO overview using ! prefix)
402
+ - \`/project:fix\` command (takes $ARGUMENTS as issue number, plans fix, implements, tests, commits)
403
+ - \`/project:sprint\` command (define acceptance criteria before coding, writes to docs/SPRINT.md)
404
+ - A TDD skill using the 3-phase isolation pattern (RED \u2192 GREEN \u2192 REFACTOR):
405
+ - RED: Write failing test only. Verify it FAILS.
406
+ - GREEN: Write MINIMUM code to pass. Nothing extra.
407
+ - REFACTOR: Improve while keeping tests green.
408
+ Rules: never write tests and implementation in same step, AAA pattern, one assertion per test.
409
+ - A multi-agent QA pipeline:
410
+ - \`@qa-orchestrator\` (sonnet) \u2014 delegates to linter and e2e-tester, compiles QA report
411
+ - \`@linter\` (haiku) \u2014 runs formatters, linters, security scanners
412
+ - \`@e2e-tester\` (sonnet, only when Playwright is in tools) \u2014 browser-based QA via Playwright
249
413
 
250
414
  ## For Research Projects, Additionally Include
251
415
 
@@ -284,7 +448,10 @@ Return ONLY valid JSON matching this structure:
284
448
  },
285
449
  "commands": {
286
450
  "help": "markdown content for /project:help",
287
- "tasks": "markdown content for /project:tasks"
451
+ "tasks": "markdown content for /project:tasks",
452
+ "status": "Show project status:\\n\\n!git status --short\\n\\n!git log --oneline -5\\n\\nRead TODO.md and summarize progress.",
453
+ "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",
454
+ "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."
288
455
  },
289
456
  "rules": {
290
457
  "continuity": "markdown content for continuity rule",
@@ -294,12 +461,15 @@ Return ONLY valid JSON matching this structure:
294
461
  "skill-name/SKILL": "markdown content with YAML frontmatter"
295
462
  },
296
463
  "agents": {
297
- "agent-name": "markdown content with YAML frontmatter"
464
+ "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.",
465
+ "linter": "---\\nname: linter\\ndescription: Fast static analysis\\nmodel: haiku\\n---\\nRun available linters (eslint, prettier, biome, ruff, mypy, semgrep). Report issues.",
466
+ "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."
298
467
  },
299
468
  "docs": {
300
469
  "TODO": "# TODO\\n\\n- [ ] First task based on workflow",
301
470
  "DECISIONS": "# Decisions\\n\\nArchitectural decisions for this project.",
302
- "LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries and gotchas."
471
+ "LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries and gotchas.",
472
+ "SPRINT": "# Sprint Contract\\n\\nDefine acceptance criteria before starting work."
303
473
  }
304
474
  }
305
475
  }
@@ -435,6 +605,24 @@ async function callLLM(config, userMessage) {
435
605
  }
436
606
  throw new Error(`Unsupported provider: ${config.provider}. Run \`kairn init\` to reconfigure.`);
437
607
  }
608
+ function validateSpec(spec, onProgress) {
609
+ const warnings = [];
610
+ if (spec.tools.length > 8) {
611
+ warnings.push(`${spec.tools.length} MCP servers selected (recommended: \u22646)`);
612
+ }
613
+ if (spec.harness.claude_md) {
614
+ const lines = spec.harness.claude_md.split("\n").length;
615
+ if (lines > 150) {
616
+ warnings.push(`CLAUDE.md is ${lines} lines (recommended: \u2264100)`);
617
+ }
618
+ }
619
+ if (spec.harness.skills && Object.keys(spec.harness.skills).length > 5) {
620
+ warnings.push(`${Object.keys(spec.harness.skills).length} skills (recommended: \u22643)`);
621
+ }
622
+ for (const warning of warnings) {
623
+ onProgress?.(`\u26A0 ${warning}`);
624
+ }
625
+ }
438
626
  async function compile(intent, onProgress) {
439
627
  const config = await loadConfig();
440
628
  if (!config) {
@@ -453,6 +641,7 @@ async function compile(intent, onProgress) {
453
641
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
454
642
  ...parsed
455
643
  };
644
+ validateSpec(spec, onProgress);
456
645
  await ensureDirs();
457
646
  const envPath = path2.join(getEnvsDir(), `${spec.id}.json`);
458
647
  await fs2.writeFile(envPath, JSON.stringify(spec, null, 2), "utf-8");
@@ -466,6 +655,50 @@ async function writeFile(filePath, content) {
466
655
  await fs3.mkdir(path3.dirname(filePath), { recursive: true });
467
656
  await fs3.writeFile(filePath, content, "utf-8");
468
657
  }
658
+ function buildFileMap(spec) {
659
+ const files = /* @__PURE__ */ new Map();
660
+ if (spec.harness.claude_md) {
661
+ files.set(".claude/CLAUDE.md", spec.harness.claude_md);
662
+ }
663
+ if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
664
+ files.set(
665
+ ".claude/settings.json",
666
+ JSON.stringify(spec.harness.settings, null, 2)
667
+ );
668
+ }
669
+ if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
670
+ files.set(
671
+ ".mcp.json",
672
+ JSON.stringify({ mcpServers: spec.harness.mcp_config }, null, 2)
673
+ );
674
+ }
675
+ if (spec.harness.commands) {
676
+ for (const [name, content] of Object.entries(spec.harness.commands)) {
677
+ files.set(`.claude/commands/${name}.md`, content);
678
+ }
679
+ }
680
+ if (spec.harness.rules) {
681
+ for (const [name, content] of Object.entries(spec.harness.rules)) {
682
+ files.set(`.claude/rules/${name}.md`, content);
683
+ }
684
+ }
685
+ if (spec.harness.skills) {
686
+ for (const [skillPath, content] of Object.entries(spec.harness.skills)) {
687
+ files.set(`.claude/skills/${skillPath}.md`, content);
688
+ }
689
+ }
690
+ if (spec.harness.agents) {
691
+ for (const [name, content] of Object.entries(spec.harness.agents)) {
692
+ files.set(`.claude/agents/${name}.md`, content);
693
+ }
694
+ }
695
+ if (spec.harness.docs) {
696
+ for (const [name, content] of Object.entries(spec.harness.docs)) {
697
+ files.set(`.claude/docs/${name}.md`, content);
698
+ }
699
+ }
700
+ return files;
701
+ }
469
702
  async function writeEnvironment(spec, targetDir) {
470
703
  const claudeDir = path3.join(targetDir, ".claude");
471
704
  const written = [];
@@ -1007,6 +1240,58 @@ async function scanProject(dir) {
1007
1240
  }
1008
1241
 
1009
1242
  // src/commands/optimize.ts
1243
+ function simpleDiff(oldContent, newContent) {
1244
+ const oldLines = oldContent.split("\n");
1245
+ const newLines = newContent.split("\n");
1246
+ const output = [];
1247
+ const maxLines = Math.max(oldLines.length, newLines.length);
1248
+ for (let i = 0; i < maxLines; i++) {
1249
+ const oldLine = oldLines[i];
1250
+ const newLine = newLines[i];
1251
+ if (oldLine === void 0) {
1252
+ output.push(chalk6.green(`+ ${newLine}`));
1253
+ } else if (newLine === void 0) {
1254
+ output.push(chalk6.red(`- ${oldLine}`));
1255
+ } else if (oldLine !== newLine) {
1256
+ output.push(chalk6.red(`- ${oldLine}`));
1257
+ output.push(chalk6.green(`+ ${newLine}`));
1258
+ }
1259
+ }
1260
+ return output;
1261
+ }
1262
+ async function generateDiff(spec, targetDir) {
1263
+ const fileMap = buildFileMap(spec);
1264
+ const results = [];
1265
+ for (const [relativePath, newContent] of fileMap) {
1266
+ const absolutePath = path9.join(targetDir, relativePath);
1267
+ let oldContent = null;
1268
+ try {
1269
+ oldContent = await fs9.readFile(absolutePath, "utf-8");
1270
+ } catch {
1271
+ }
1272
+ if (oldContent === null) {
1273
+ results.push({
1274
+ path: relativePath,
1275
+ status: "new",
1276
+ diff: chalk6.green("+ NEW FILE")
1277
+ });
1278
+ } else if (oldContent === newContent) {
1279
+ results.push({
1280
+ path: relativePath,
1281
+ status: "unchanged",
1282
+ diff: ""
1283
+ });
1284
+ } else {
1285
+ const diffLines = simpleDiff(oldContent, newContent);
1286
+ results.push({
1287
+ path: relativePath,
1288
+ status: "modified",
1289
+ diff: diffLines.join("\n")
1290
+ });
1291
+ }
1292
+ }
1293
+ return results;
1294
+ }
1010
1295
  async function loadRegistry3() {
1011
1296
  const __filename = fileURLToPath4(import.meta.url);
1012
1297
  const __dirname = path9.dirname(__filename);
@@ -1080,6 +1365,10 @@ ${profile.existingClaudeMd}`);
1080
1365
  parts.push("- Are security rules present?");
1081
1366
  parts.push("- Is there a continuity rule for session memory?");
1082
1367
  parts.push("- Are there unnecessary MCP servers adding context bloat?");
1368
+ parts.push("- Are hooks configured in settings.json for destructive command blocking?");
1369
+ parts.push("- Are there path-scoped rules for different code domains (api, testing, frontend)?");
1370
+ parts.push("- Does the project have a /project:status command with live git output?");
1371
+ parts.push("- Is there a /project:fix command for issue-driven development?");
1083
1372
  if (profile.claudeMdLineCount > 200) {
1084
1373
  parts.push(`- CLAUDE.md is ${profile.claudeMdLineCount} lines \u2014 needs aggressive trimming`);
1085
1374
  }
@@ -1099,7 +1388,7 @@ ${profile.existingClaudeMd}`);
1099
1388
  }
1100
1389
  return parts.join("\n");
1101
1390
  }
1102
- 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) => {
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) => {
1103
1392
  const config = await loadConfig();
1104
1393
  if (!config) {
1105
1394
  console.log(
@@ -1136,6 +1425,9 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1136
1425
  if (profile.mcpServerCount === 0 && profile.dependencies.length > 0) issues.push("No MCP servers configured");
1137
1426
  if (profile.hasTests && !profile.existingCommands.includes("test")) issues.push("Has tests but no /project:test command");
1138
1427
  if (!profile.existingCommands.includes("tasks")) issues.push("Missing /project:tasks command");
1428
+ if (!profile.existingSettings?.hooks) issues.push("No hooks configured \u2014 missing destructive command blocking");
1429
+ const scopedRules = profile.existingRules.filter((r) => r !== "security" && r !== "continuity");
1430
+ if (profile.hasSrc && scopedRules.length === 0) issues.push("No path-scoped rules \u2014 consider adding api.md, testing.md, or frontend.md rules");
1139
1431
  if (issues.length > 0) {
1140
1432
  console.log(chalk6.yellow("\n Issues Found:\n"));
1141
1433
  for (const issue of issues) {
@@ -1210,6 +1502,34 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1210
1502
  console.log(chalk6.yellow(` ${cmd}`));
1211
1503
  }
1212
1504
  }
1505
+ if (options.diff) {
1506
+ const diffs = await generateDiff(spec, targetDir);
1507
+ const changedDiffs = diffs.filter((d) => d.status !== "unchanged");
1508
+ if (changedDiffs.length === 0) {
1509
+ console.log(chalk6.green("\n \u2713 No changes needed \u2014 environment is already up to date.\n"));
1510
+ return;
1511
+ }
1512
+ console.log(chalk6.cyan("\n Changes preview:\n"));
1513
+ for (const d of changedDiffs) {
1514
+ console.log(chalk6.cyan(` --- ${d.path}`));
1515
+ if (d.status === "new") {
1516
+ console.log(` ${d.diff}`);
1517
+ } else {
1518
+ for (const line of d.diff.split("\n")) {
1519
+ console.log(` ${line}`);
1520
+ }
1521
+ }
1522
+ console.log("");
1523
+ }
1524
+ const apply = await confirm2({
1525
+ message: "Apply these changes?",
1526
+ default: true
1527
+ });
1528
+ if (!apply) {
1529
+ console.log(chalk6.dim("\n Aborted.\n"));
1530
+ return;
1531
+ }
1532
+ }
1213
1533
  const written = await writeEnvironment(spec, targetDir);
1214
1534
  console.log(chalk6.green("\n \u2713 Environment written\n"));
1215
1535
  for (const file of written) {
@@ -1240,16 +1560,185 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1240
1560
  );
1241
1561
  });
1242
1562
 
1563
+ // src/commands/doctor.ts
1564
+ import { Command as Command7 } from "commander";
1565
+ import chalk7 from "chalk";
1566
+ function runChecks(profile) {
1567
+ const checks = [];
1568
+ if (!profile.existingClaudeMd) {
1569
+ checks.push({
1570
+ name: "CLAUDE.md",
1571
+ weight: 3,
1572
+ status: "fail",
1573
+ message: "Missing CLAUDE.md"
1574
+ });
1575
+ } else if (profile.claudeMdLineCount > 200) {
1576
+ checks.push({
1577
+ name: "CLAUDE.md",
1578
+ weight: 2,
1579
+ status: "warn",
1580
+ message: `${profile.claudeMdLineCount} lines (recommended: \u2264100)`
1581
+ });
1582
+ } else {
1583
+ checks.push({
1584
+ name: "CLAUDE.md",
1585
+ weight: 3,
1586
+ status: "pass",
1587
+ message: `${profile.claudeMdLineCount} lines`
1588
+ });
1589
+ }
1590
+ if (!profile.existingSettings) {
1591
+ checks.push({
1592
+ name: "settings.json",
1593
+ weight: 2,
1594
+ status: "fail",
1595
+ message: "Missing settings.json"
1596
+ });
1597
+ } else {
1598
+ const perms2 = profile.existingSettings.permissions;
1599
+ const hasDeny = perms2?.deny && Array.isArray(perms2.deny) && perms2.deny.length > 0;
1600
+ checks.push({
1601
+ name: "Deny rules",
1602
+ weight: 2,
1603
+ status: hasDeny ? "pass" : "warn",
1604
+ message: hasDeny ? "Deny rules configured" : "No deny rules in settings.json"
1605
+ });
1606
+ }
1607
+ if (profile.mcpServerCount > 8) {
1608
+ checks.push({
1609
+ name: "MCP servers",
1610
+ weight: 1,
1611
+ status: "warn",
1612
+ message: `${profile.mcpServerCount} servers (recommended: \u22648)`
1613
+ });
1614
+ } else if (profile.mcpServerCount > 0) {
1615
+ checks.push({
1616
+ name: "MCP servers",
1617
+ weight: 1,
1618
+ status: "pass",
1619
+ message: `${profile.mcpServerCount} servers`
1620
+ });
1621
+ } else {
1622
+ checks.push({
1623
+ name: "MCP servers",
1624
+ weight: 1,
1625
+ status: "warn",
1626
+ message: "No MCP servers configured"
1627
+ });
1628
+ }
1629
+ checks.push({
1630
+ name: "/project:help",
1631
+ weight: 2,
1632
+ status: profile.existingCommands.includes("help") ? "pass" : "fail",
1633
+ message: profile.existingCommands.includes("help") ? "Help command present" : "Missing /project:help command"
1634
+ });
1635
+ checks.push({
1636
+ name: "/project:tasks",
1637
+ weight: 1,
1638
+ status: profile.existingCommands.includes("tasks") ? "pass" : "warn",
1639
+ message: profile.existingCommands.includes("tasks") ? "Tasks command present" : "Missing /project:tasks command"
1640
+ });
1641
+ checks.push({
1642
+ name: "Security rule",
1643
+ weight: 3,
1644
+ status: profile.existingRules.includes("security") ? "pass" : "fail",
1645
+ message: profile.existingRules.includes("security") ? "Security rule present" : "Missing rules/security.md"
1646
+ });
1647
+ checks.push({
1648
+ name: "Continuity rule",
1649
+ weight: 2,
1650
+ status: profile.existingRules.includes("continuity") ? "pass" : "warn",
1651
+ message: profile.existingRules.includes("continuity") ? "Continuity rule present" : "Missing rules/continuity.md"
1652
+ });
1653
+ const hasHooks = profile.existingSettings?.hooks;
1654
+ checks.push({
1655
+ name: "Hooks",
1656
+ weight: 1,
1657
+ status: hasHooks ? "pass" : "warn",
1658
+ message: hasHooks ? "Hooks configured" : "No hooks in settings.json"
1659
+ });
1660
+ const perms = profile.existingSettings?.permissions;
1661
+ const denyList = perms?.deny || [];
1662
+ const envProtected = denyList.some((d) => d.includes(".env"));
1663
+ checks.push({
1664
+ name: ".env protection",
1665
+ weight: 2,
1666
+ status: envProtected ? "pass" : "warn",
1667
+ message: envProtected ? ".env in deny list" : ".env not in deny list"
1668
+ });
1669
+ if (profile.existingClaudeMd) {
1670
+ const requiredSections = ["## Purpose", "## Commands", "## Tech Stack"];
1671
+ const missingSections = requiredSections.filter(
1672
+ (s) => !profile.existingClaudeMd.includes(s)
1673
+ );
1674
+ if (missingSections.length > 0) {
1675
+ checks.push({
1676
+ name: "CLAUDE.md sections",
1677
+ weight: 1,
1678
+ status: "warn",
1679
+ message: `Missing: ${missingSections.join(", ")}`
1680
+ });
1681
+ } else {
1682
+ checks.push({
1683
+ name: "CLAUDE.md sections",
1684
+ weight: 1,
1685
+ status: "pass",
1686
+ message: "Required sections present"
1687
+ });
1688
+ }
1689
+ }
1690
+ return checks;
1691
+ }
1692
+ var doctorCommand = new Command7("doctor").description(
1693
+ "Validate the current Claude Code environment against best practices"
1694
+ ).action(async () => {
1695
+ const targetDir = process.cwd();
1696
+ console.log(chalk7.dim("\n Checking .claude/ environment...\n"));
1697
+ const profile = await scanProject(targetDir);
1698
+ if (!profile.hasClaudeDir) {
1699
+ console.log(chalk7.red(" \u274C No .claude/ directory found.\n"));
1700
+ console.log(
1701
+ chalk7.dim(" Run ") + chalk7.bold("kairn describe") + chalk7.dim(" or ") + chalk7.bold("kairn optimize") + chalk7.dim(" to generate one.\n")
1702
+ );
1703
+ process.exit(1);
1704
+ }
1705
+ const checks = runChecks(profile);
1706
+ for (const check of checks) {
1707
+ const icon = check.status === "pass" ? chalk7.green("\u2705") : check.status === "warn" ? chalk7.yellow("\u26A0\uFE0F ") : chalk7.red("\u274C");
1708
+ const msg = check.status === "pass" ? chalk7.dim(check.message) : check.status === "warn" ? chalk7.yellow(check.message) : chalk7.red(check.message);
1709
+ console.log(` ${icon} ${check.name}: ${msg}`);
1710
+ }
1711
+ const maxScore = checks.reduce((sum, c) => sum + c.weight, 0);
1712
+ const score = checks.reduce((sum, c) => {
1713
+ if (c.status === "pass") return sum + c.weight;
1714
+ if (c.status === "warn") return sum + Math.floor(c.weight / 2);
1715
+ return sum;
1716
+ }, 0);
1717
+ const percentage = Math.round(score / maxScore * 100);
1718
+ const scoreColor = percentage >= 80 ? chalk7.green : percentage >= 50 ? chalk7.yellow : chalk7.red;
1719
+ console.log(
1720
+ `
1721
+ Score: ${scoreColor(`${score}/${maxScore}`)} (${scoreColor(`${percentage}%`)})
1722
+ `
1723
+ );
1724
+ if (percentage < 80) {
1725
+ console.log(
1726
+ chalk7.dim(" Run ") + chalk7.bold("kairn optimize") + chalk7.dim(" to fix issues.\n")
1727
+ );
1728
+ }
1729
+ });
1730
+
1243
1731
  // src/cli.ts
1244
- var program = new Command7();
1732
+ var program = new Command8();
1245
1733
  program.name("kairn").description(
1246
1734
  "Compile natural language intent into optimized Claude Code environments"
1247
- ).version("1.0.0");
1735
+ ).version("1.4.0");
1248
1736
  program.addCommand(initCommand);
1249
1737
  program.addCommand(describeCommand);
1250
1738
  program.addCommand(optimizeCommand);
1251
1739
  program.addCommand(listCommand);
1252
1740
  program.addCommand(activateCommand);
1253
1741
  program.addCommand(updateRegistryCommand);
1742
+ program.addCommand(doctorCommand);
1254
1743
  program.parse();
1255
1744
  //# sourceMappingURL=cli.js.map