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 +498 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/registry/tools.json +322 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { Command as
|
|
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
|
-
-
|
|
248
|
-
-
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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
|