kairn-cli 1.6.0 → 1.8.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,6 +1,6 @@
1
1
  // src/cli.ts
2
- import { Command as Command10 } from "commander";
3
- import chalk12 from "chalk";
2
+ import { Command as Command11 } from "commander";
3
+ import chalk14 from "chalk";
4
4
 
5
5
  // src/commands/init.ts
6
6
  import { Command } from "commander";
@@ -90,7 +90,7 @@ var ui = {
90
90
  // Key-value pairs
91
91
  kv: (key, value) => ` ${chalk.cyan(key.padEnd(14))} ${value}`,
92
92
  // File list
93
- file: (path13) => chalk.dim(` ${path13}`),
93
+ file: (path15) => chalk.dim(` ${path15}`),
94
94
  // Tool display
95
95
  tool: (name, reason) => ` ${warm("\u25CF")} ${chalk.bold(name)}
96
96
  ${chalk.dim(reason)}`,
@@ -339,7 +339,7 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
339
339
  // src/commands/describe.ts
340
340
  import { Command as Command2 } from "commander";
341
341
  import { input, confirm } from "@inquirer/prompts";
342
- import chalk4 from "chalk";
342
+ import chalk5 from "chalk";
343
343
  import ora from "ora";
344
344
 
345
345
  // src/compiler/compile.ts
@@ -362,12 +362,12 @@ You must output a JSON object matching the EnvironmentSpec schema.
362
362
 
363
363
  - **Minimalism over completeness.** Fewer, well-chosen tools beat many generic ones. Each MCP server costs 500-2000 context tokens.
364
364
  - **Workflow-specific, not generic.** Every instruction, command, and rule must relate to the user's actual workflow.
365
- - **Concise CLAUDE.md.** Under 100 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
365
+ - **Concise CLAUDE.md.** Under 120 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
366
366
  - **Security by default.** Always include deny rules for destructive commands and secret file access.
367
367
 
368
368
  ## CLAUDE.md Template (mandatory structure)
369
369
 
370
- The \`claude_md\` field MUST follow this exact structure (max 100 lines):
370
+ The \`claude_md\` field MUST follow this exact structure (max 120 lines):
371
371
 
372
372
  \`\`\`
373
373
  # {Project Name}
@@ -392,6 +392,30 @@ The \`claude_md\` field MUST follow this exact structure (max 100 lines):
392
392
 
393
393
  ## Output
394
394
  {where results go, key files}
395
+
396
+ ## Verification
397
+ After implementing any change, verify it works:
398
+ - {build command} \u2014 must pass with no errors
399
+ - {test command} \u2014 all tests must pass
400
+ - {lint command} \u2014 no warnings or errors
401
+ - {type check command} \u2014 no type errors
402
+
403
+ If any verification step fails, fix the issue before moving on.
404
+ Do NOT skip verification steps.
405
+
406
+ ## Known Gotchas
407
+ <!-- After any correction, add it here: "Update CLAUDE.md so you don't make that mistake again." -->
408
+ <!-- Prune this section when it exceeds 10 items \u2014 keep only the recurring ones. -->
409
+ - (none yet \u2014 this section grows as you work)
410
+
411
+ ## Debugging
412
+ When debugging, paste raw error output. Don't summarize \u2014 Claude works better with raw data.
413
+ Use subagents for deep investigation to keep main context clean.
414
+
415
+ ## Git Workflow
416
+ - Prefer small, focused commits (one feature or fix per commit)
417
+ - Use conventional commits: feat:, fix:, docs:, refactor:, test:
418
+ - Target < 200 lines per PR when possible
395
419
  \`\`\`
396
420
 
397
421
  Do not add generic filler. Every line must be specific to the user's workflow.
@@ -410,6 +434,10 @@ Do not add generic filler. Every line must be specific to the user's workflow.
410
434
  10. A \`/project:status\` command for code projects (uses ! for live git/test output)
411
435
  11. A \`/project:fix\` command for code projects (uses $ARGUMENTS for issue number)
412
436
  12. A \`docs/SPRINT.md\` file for sprint contracts (acceptance criteria, verification steps)
437
+ 13. A "Verification" section in CLAUDE.md with concrete verify commands for the project
438
+ 14. A "Known Gotchas" section in CLAUDE.md (starts empty, grows with corrections)
439
+ 15. A "Debugging" section in CLAUDE.md (2 lines: paste raw errors, use subagents)
440
+ 16. A "Git Workflow" section in CLAUDE.md (3 rules: small commits, conventional format, <200 lines PR)
413
441
 
414
442
  ## Shell-Integrated Commands
415
443
 
@@ -530,7 +558,7 @@ Merge this into the settings hooks alongside the PreToolUse and PostToolUse hook
530
558
  ## Context Budget (STRICT)
531
559
 
532
560
  - MCP servers: maximum 6. Prefer fewer.
533
- - CLAUDE.md: maximum 100 lines.
561
+ - CLAUDE.md: maximum 120 lines.
534
562
  - Rules: maximum 5 files, each under 20 lines.
535
563
  - Skills: maximum 3. Only include directly relevant ones.
536
564
  - Agents: maximum 3. QA pipeline + one specialist.
@@ -558,6 +586,10 @@ Each MCP server costs 500-2000 tokens of context window.
558
586
  - \`@qa-orchestrator\` (sonnet) \u2014 delegates to linter and e2e-tester, compiles QA report
559
587
  - \`@linter\` (haiku) \u2014 runs formatters, linters, security scanners
560
588
  - \`@e2e-tester\` (sonnet, only when Playwright is in tools) \u2014 browser-based QA via Playwright
589
+ - \`/project:spec\` command (interview-based spec creation \u2014 asks 5-8 questions one at a time, writes structured spec to docs/SPRINT.md, does NOT start coding until confirmed)
590
+ - \`/project:prove\` command (runs tests, shows git diff vs main, rates confidence HIGH/MEDIUM/LOW with evidence)
591
+ - \`/project:grill\` command (adversarial code review \u2014 challenges each change with "why this approach?", "what if X input?", rates BLOCKER/SHOULD-FIX/NITPICK, blocks until BLOCKERs resolved)
592
+ - \`/project:reset\` command (reads DECISIONS.md and LEARNINGS.md, proposes clean restart, stashes current work, implements elegant solution)
561
593
 
562
594
  ## For Research Projects, Additionally Include
563
595
 
@@ -565,6 +597,7 @@ Each MCP server costs 500-2000 tokens of context window.
565
597
  - \`/project:summarize\` command (summarize findings)
566
598
  - A research-synthesis skill
567
599
  - A researcher agent
600
+ - Note: the Verification section in CLAUDE.md should adapt for research \u2014 e.g. "Verify all sources are cited" instead of build/test commands
568
601
 
569
602
  ## For Content/Writing Projects, Additionally Include
570
603
 
@@ -593,7 +626,7 @@ Return ONLY valid JSON matching this structure:
593
626
  { "tool_id": "id-from-registry", "reason": "why this tool fits" }
594
627
  ],
595
628
  "harness": {
596
- "claude_md": "The full CLAUDE.md content (under 100 lines)",
629
+ "claude_md": "The full CLAUDE.md content (under 120 lines)",
597
630
  "settings": {
598
631
  "permissions": {
599
632
  "allow": ["Bash(npm run *)", "Read", "Write", "Edit"],
@@ -608,7 +641,11 @@ Return ONLY valid JSON matching this structure:
608
641
  "tasks": "markdown content for /project:tasks",
609
642
  "status": "Show project status:\\n\\n!git status --short\\n\\n!git log --oneline -5\\n\\nRead TODO.md and summarize progress.",
610
643
  "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",
611
- "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."
644
+ "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.",
645
+ "spec": "Before building this feature, interview me to create a complete spec.\\n\\nAsk me 5-8 questions, one at a time:\\n1. What specifically should this feature do?\\n2. Who uses it and how?\\n3. What are the edge cases or error states?\\n4. How will we know it works? (acceptance criteria)\\n5. What should it explicitly NOT do? (scope boundaries)\\n6. Any dependencies, APIs, or constraints?\\n7. How does it fit with existing code?\\n8. Priority: speed, quality, or flexibility?\\n\\nAfter my answers, write a structured spec to docs/SPRINT.md:\\n- Feature name\\n- Description (from my answers, not invented)\\n- Acceptance criteria (testable)\\n- Out of scope\\n- Technical approach\\n\\nDo NOT start coding until I confirm the spec.",
646
+ "prove": "Prove the current implementation works.\\n\\n1. Run the full test suite:\\n\\n!npm test 2>&1\\n\\n2. Compare against main:\\n\\n!git diff main --stat 2>/dev/null\\n\\n3. Show evidence:\\n - Test results (pass/fail counts)\\n - Behavioral diff (main vs this branch)\\n - Edge cases tested\\n - Error handling verified\\n\\n4. Rate confidence:\\n - HIGH: All tests pass, edge cases covered, no regressions\\n - MEDIUM: Core works, some edges untested\\n - LOW: Needs more verification\\n\\nIf LOW or MEDIUM, explain what's missing and fix it.",
647
+ "grill": "Review the current changes adversarially.\\n\\n!git diff --staged 2>/dev/null || git diff HEAD 2>/dev/null\\n\\nAct as a senior engineer. For each file changed:\\n\\n1. \\"Why this approach over X?\\"\\n2. \\"What happens if Y input?\\"\\n3. \\"Performance impact of Z?\\"\\n4. \\"This could break if...\\"\\n\\nFor each concern:\\n- Severity: BLOCKER / SHOULD-FIX / NITPICK\\n- The exact scenario that could fail\\n- A suggested alternative\\n\\nDo NOT approve until all BLOCKERs are resolved.",
648
+ "reset": "Stop. Read docs/DECISIONS.md and docs/LEARNINGS.md.\\n\\nConsidering everything we've learned:\\n1. What was the original approach?\\n2. What went wrong or feels inelegant?\\n3. What would the clean solution look like?\\n\\nPropose the new approach. Do NOT implement yet.\\nIf I approve, stash current changes:\\n git stash -m \\"pre-reset: $(date +%Y%m%d-%H%M)\\"\\n\\nThen implement the elegant solution."
612
649
  },
613
650
  "rules": {
614
651
  "continuity": "markdown content for continuity rule",
@@ -882,20 +919,48 @@ async function generateClarifications(intent, onProgress) {
882
919
  // src/adapter/claude-code.ts
883
920
  import fs5 from "fs/promises";
884
921
  import path5 from "path";
922
+ var STATUS_LINE = {
923
+ command: `printf '%s | %s tasks' "$(git branch --show-current 2>/dev/null || echo 'no-git')" "$(grep -c '\\- \\[ \\]' docs/TODO.md 2>/dev/null || echo 0)"`
924
+ };
925
+ function isCodeProject(spec) {
926
+ const commands = spec.harness.commands ?? {};
927
+ return "status" in commands || "test" in commands;
928
+ }
929
+ var ENV_LOADER_HOOK = {
930
+ matcher: "",
931
+ hooks: [{
932
+ type: "command",
933
+ command: 'if [ -f .env ] && [ -n "$CLAUDE_ENV_FILE" ]; then grep -v "^#" .env | grep -v "^$" | grep "=" >> "$CLAUDE_ENV_FILE"; fi'
934
+ }]
935
+ };
936
+ function resolveSettings(spec, options) {
937
+ const settings = spec.harness.settings;
938
+ const base = settings && Object.keys(settings).length > 0 ? { ...settings } : {};
939
+ if (!("statusLine" in base) && isCodeProject(spec)) {
940
+ base.statusLine = STATUS_LINE;
941
+ }
942
+ if (options?.hasEnvVars) {
943
+ const hooks = base.hooks ?? {};
944
+ const sessionStart = hooks.SessionStart ?? [];
945
+ sessionStart.push(ENV_LOADER_HOOK);
946
+ hooks.SessionStart = sessionStart;
947
+ base.hooks = hooks;
948
+ }
949
+ if (Object.keys(base).length === 0) return null;
950
+ return base;
951
+ }
885
952
  async function writeFile(filePath, content) {
886
953
  await fs5.mkdir(path5.dirname(filePath), { recursive: true });
887
954
  await fs5.writeFile(filePath, content, "utf-8");
888
955
  }
889
- function buildFileMap(spec) {
956
+ function buildFileMap(spec, options) {
890
957
  const files = /* @__PURE__ */ new Map();
891
958
  if (spec.harness.claude_md) {
892
959
  files.set(".claude/CLAUDE.md", spec.harness.claude_md);
893
960
  }
894
- if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
895
- files.set(
896
- ".claude/settings.json",
897
- JSON.stringify(spec.harness.settings, null, 2)
898
- );
961
+ const resolvedSettings = resolveSettings(spec, options);
962
+ if (resolvedSettings) {
963
+ files.set(".claude/settings.json", JSON.stringify(resolvedSettings, null, 2));
899
964
  }
900
965
  if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
901
966
  files.set(
@@ -930,7 +995,7 @@ function buildFileMap(spec) {
930
995
  }
931
996
  return files;
932
997
  }
933
- async function writeEnvironment(spec, targetDir) {
998
+ async function writeEnvironment(spec, targetDir, options) {
934
999
  const claudeDir = path5.join(targetDir, ".claude");
935
1000
  const written = [];
936
1001
  if (spec.harness.claude_md) {
@@ -938,9 +1003,10 @@ async function writeEnvironment(spec, targetDir) {
938
1003
  await writeFile(p, spec.harness.claude_md);
939
1004
  written.push(".claude/CLAUDE.md");
940
1005
  }
941
- if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
1006
+ const resolvedSettings = resolveSettings(spec, options);
1007
+ if (resolvedSettings) {
942
1008
  const p = path5.join(claudeDir, "settings.json");
943
- await writeFile(p, JSON.stringify(spec.harness.settings, null, 2));
1009
+ await writeFile(p, JSON.stringify(resolvedSettings, null, 2));
944
1010
  written.push(".claude/settings.json");
945
1011
  }
946
1012
  if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
@@ -1140,6 +1206,114 @@ async function writeHermesEnvironment(spec, registry) {
1140
1206
  return written;
1141
1207
  }
1142
1208
 
1209
+ // src/secrets.ts
1210
+ import { password as password2 } from "@inquirer/prompts";
1211
+ import chalk4 from "chalk";
1212
+ import fs7 from "fs/promises";
1213
+ import path7 from "path";
1214
+ async function collectAndWriteKeys(envSetup, targetDir) {
1215
+ console.log(ui.section("API Keys"));
1216
+ console.log(
1217
+ chalk4.dim(" Some tools need API keys. Enter them now or press Enter to skip.\n")
1218
+ );
1219
+ const envEntries = [
1220
+ "# Generated by Kairn \u2014 API keys for MCP servers",
1221
+ "# Do NOT commit this file to git",
1222
+ ""
1223
+ ];
1224
+ let keysEntered = 0;
1225
+ let keysSkipped = 0;
1226
+ const seen = /* @__PURE__ */ new Set();
1227
+ for (const env of envSetup) {
1228
+ if (seen.has(env.envVar)) continue;
1229
+ seen.add(env.envVar);
1230
+ console.log(chalk4.bold(` ${env.envVar}`) + chalk4.dim(` (${env.toolName})`));
1231
+ if (env.signupUrl) {
1232
+ console.log(chalk4.dim(` Get one at: ${env.signupUrl}`));
1233
+ }
1234
+ const value = await password2({
1235
+ message: env.envVar,
1236
+ mask: "\u2022"
1237
+ });
1238
+ if (value && value.trim()) {
1239
+ envEntries.push(`${env.envVar}=${value.trim()}`);
1240
+ console.log(chalk4.green(" \u2713 saved\n"));
1241
+ keysEntered++;
1242
+ } else {
1243
+ envEntries.push(`${env.envVar}=`);
1244
+ console.log(chalk4.dim(" (skipped)\n"));
1245
+ keysSkipped++;
1246
+ }
1247
+ }
1248
+ const envPath = path7.join(targetDir, ".env");
1249
+ await fs7.writeFile(envPath, envEntries.join("\n") + "\n", "utf-8");
1250
+ await ensureGitignoreEntry(targetDir, ".env");
1251
+ console.log(chalk4.green(` \u2713 ${keysEntered} key(s) saved to .env (gitignored)`));
1252
+ if (keysSkipped > 0) {
1253
+ console.log(chalk4.dim(" Skipped keys can be added later: kairn keys"));
1254
+ }
1255
+ return { keysEntered, keysSkipped, envPath };
1256
+ }
1257
+ async function writeEmptyEnvFile(envSetup, targetDir) {
1258
+ const envEntries = [
1259
+ "# Generated by Kairn \u2014 API keys for MCP servers",
1260
+ "# Do NOT commit this file to git",
1261
+ ""
1262
+ ];
1263
+ const seen = /* @__PURE__ */ new Set();
1264
+ for (const env of envSetup) {
1265
+ if (seen.has(env.envVar)) continue;
1266
+ seen.add(env.envVar);
1267
+ envEntries.push(`${env.envVar}=`);
1268
+ }
1269
+ const envPath = path7.join(targetDir, ".env");
1270
+ await fs7.writeFile(envPath, envEntries.join("\n") + "\n", "utf-8");
1271
+ await ensureGitignoreEntry(targetDir, ".env");
1272
+ }
1273
+ async function ensureGitignoreEntry(targetDir, entry) {
1274
+ const gitignorePath = path7.join(targetDir, ".gitignore");
1275
+ let gitignore = "";
1276
+ try {
1277
+ gitignore = await fs7.readFile(gitignorePath, "utf-8");
1278
+ } catch {
1279
+ }
1280
+ if (!gitignore.split("\n").some((line) => line.trim() === entry)) {
1281
+ const separator = gitignore.length > 0 && !gitignore.endsWith("\n") ? "\n" : "";
1282
+ await fs7.writeFile(gitignorePath, gitignore + separator + entry + "\n", "utf-8");
1283
+ }
1284
+ }
1285
+ async function readEnvFile(targetDir) {
1286
+ const envPath = path7.join(targetDir, ".env");
1287
+ const entries = /* @__PURE__ */ new Map();
1288
+ try {
1289
+ const content = await fs7.readFile(envPath, "utf-8");
1290
+ for (const line of content.split("\n")) {
1291
+ const trimmed = line.trim();
1292
+ if (!trimmed || trimmed.startsWith("#")) continue;
1293
+ const eqIndex = trimmed.indexOf("=");
1294
+ if (eqIndex === -1) continue;
1295
+ const key = trimmed.slice(0, eqIndex);
1296
+ const value = trimmed.slice(eqIndex + 1);
1297
+ entries.set(key, value);
1298
+ }
1299
+ } catch {
1300
+ }
1301
+ return entries;
1302
+ }
1303
+ async function detectRequiredEnvVars(targetDir) {
1304
+ const mcpPath = path7.join(targetDir, ".mcp.json");
1305
+ const envVars = /* @__PURE__ */ new Set();
1306
+ try {
1307
+ const content = await fs7.readFile(mcpPath, "utf-8");
1308
+ const matches = content.matchAll(/\$\{([A-Z_][A-Z0-9_]*)\}/g);
1309
+ for (const match of matches) {
1310
+ envVars.add(match[1]);
1311
+ }
1312
+ } catch {
1313
+ }
1314
+ return [...envVars];
1315
+ }
1316
+
1143
1317
  // src/commands/describe.ts
1144
1318
  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("-q, --quick", "Skip clarification questions").option("--runtime <runtime>", "Target runtime (claude-code or hermes)", "claude-code").action(async (intentArg, options) => {
1145
1319
  printFullBanner("The Agent Environment Compiler");
@@ -1148,7 +1322,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
1148
1322
  console.log(
1149
1323
  ui.errorBox(
1150
1324
  "No configuration found",
1151
- `Run ${chalk4.bold("kairn init")} to set up your API key.`
1325
+ `Run ${chalk5.bold("kairn init")} to set up your API key.`
1152
1326
  )
1153
1327
  );
1154
1328
  process.exit(1);
@@ -1157,14 +1331,14 @@ var describeCommand = new Command2("describe").description("Describe your workfl
1157
1331
  message: "What do you want your agent to do?"
1158
1332
  });
1159
1333
  if (!intentRaw.trim()) {
1160
- console.log(chalk4.red("\n No description provided. Aborting.\n"));
1334
+ console.log(chalk5.red("\n No description provided. Aborting.\n"));
1161
1335
  process.exit(1);
1162
1336
  }
1163
1337
  let finalIntent = intentRaw;
1164
1338
  if (!options.quick) {
1165
1339
  console.log(ui.section("Clarification"));
1166
- console.log(chalk4.dim(" Let me understand your project better."));
1167
- console.log(chalk4.dim(" Press Enter to accept the suggestion, or type your own answer.\n"));
1340
+ console.log(chalk5.dim(" Let me understand your project better."));
1341
+ console.log(chalk5.dim(" Press Enter to accept the suggestion, or type your own answer.\n"));
1168
1342
  let clarifications = [];
1169
1343
  try {
1170
1344
  clarifications = await generateClarifications(intentRaw);
@@ -1197,7 +1371,7 @@ ${clarificationLines}`;
1197
1371
  } catch (err) {
1198
1372
  spinner.fail("Compilation failed");
1199
1373
  const msg = err instanceof Error ? err.message : String(err);
1200
- console.log(chalk4.red(`
1374
+ console.log(chalk5.red(`
1201
1375
  ${msg}
1202
1376
  `));
1203
1377
  process.exit(1);
@@ -1227,7 +1401,7 @@ ${clarificationLines}`;
1227
1401
  default: true
1228
1402
  });
1229
1403
  if (!proceed) {
1230
- console.log(chalk4.dim("\n Aborted. Environment saved to ~/.kairn/envs/\n"));
1404
+ console.log(chalk5.dim("\n Aborted. Environment saved to ~/.kairn/envs/\n"));
1231
1405
  return;
1232
1406
  }
1233
1407
  const targetDir = process.cwd();
@@ -1236,25 +1410,24 @@ ${clarificationLines}`;
1236
1410
  await writeHermesEnvironment(spec, registry);
1237
1411
  console.log("\n" + ui.success("Environment written for Hermes"));
1238
1412
  console.log(
1239
- chalk4.cyan("\n Ready! Run ") + chalk4.bold("hermes") + chalk4.cyan(" to start.\n")
1413
+ chalk5.cyan("\n Ready! Run ") + chalk5.bold("hermes") + chalk5.cyan(" to start.\n")
1240
1414
  );
1241
1415
  } else {
1242
- const written = await writeEnvironment(spec, targetDir);
1416
+ const hasEnvVars = summary.envSetup.length > 0;
1417
+ const written = await writeEnvironment(spec, targetDir, { hasEnvVars });
1243
1418
  console.log(ui.section("Files Written"));
1244
1419
  console.log("");
1245
1420
  for (const file of written) {
1246
1421
  console.log(ui.file(file));
1247
1422
  }
1248
- if (summary.envSetup.length > 0) {
1249
- console.log(ui.section("Setup Required"));
1250
- console.log("");
1251
- const seen = /* @__PURE__ */ new Set();
1252
- for (const env of summary.envSetup) {
1253
- if (seen.has(env.envVar)) continue;
1254
- seen.add(env.envVar);
1255
- console.log(ui.envVar(env.envVar, env.description, env.signupUrl));
1256
- console.log("");
1423
+ if (hasEnvVars) {
1424
+ if (options.quick) {
1425
+ await writeEmptyEnvFile(summary.envSetup, targetDir);
1426
+ console.log(ui.success("Empty .env written (gitignored) \u2014 fill in keys later: kairn keys"));
1427
+ } else {
1428
+ await collectAndWriteKeys(summary.envSetup, targetDir);
1257
1429
  }
1430
+ console.log("");
1258
1431
  }
1259
1432
  if (summary.pluginCommands.length > 0) {
1260
1433
  console.log(ui.section("Plugins"));
@@ -1272,28 +1445,28 @@ ${clarificationLines}`;
1272
1445
 
1273
1446
  // src/commands/list.ts
1274
1447
  import { Command as Command3 } from "commander";
1275
- import chalk5 from "chalk";
1276
- import fs7 from "fs/promises";
1277
- import path7 from "path";
1448
+ import chalk6 from "chalk";
1449
+ import fs8 from "fs/promises";
1450
+ import path8 from "path";
1278
1451
  var listCommand = new Command3("list").description("Show saved environments").action(async () => {
1279
1452
  printCompactBanner();
1280
1453
  const envsDir = getEnvsDir();
1281
1454
  let files;
1282
1455
  try {
1283
- files = await fs7.readdir(envsDir);
1456
+ files = await fs8.readdir(envsDir);
1284
1457
  } catch {
1285
- console.log(chalk5.dim(" No environments yet. Run ") + chalk5.bold("kairn describe") + chalk5.dim(" to create one.\n"));
1458
+ console.log(chalk6.dim(" No environments yet. Run ") + chalk6.bold("kairn describe") + chalk6.dim(" to create one.\n"));
1286
1459
  return;
1287
1460
  }
1288
1461
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
1289
1462
  if (jsonFiles.length === 0) {
1290
- console.log(chalk5.dim(" No environments yet. Run ") + chalk5.bold("kairn describe") + chalk5.dim(" to create one.\n"));
1463
+ console.log(chalk6.dim(" No environments yet. Run ") + chalk6.bold("kairn describe") + chalk6.dim(" to create one.\n"));
1291
1464
  return;
1292
1465
  }
1293
1466
  let first = true;
1294
1467
  for (const file of jsonFiles) {
1295
1468
  try {
1296
- const data = await fs7.readFile(path7.join(envsDir, file), "utf-8");
1469
+ const data = await fs8.readFile(path8.join(envsDir, file), "utf-8");
1297
1470
  const spec = JSON.parse(data);
1298
1471
  const date = new Date(spec.created_at).toLocaleDateString();
1299
1472
  const toolCount = spec.tools?.length ?? 0;
@@ -1301,10 +1474,10 @@ var listCommand = new Command3("list").description("Show saved environments").ac
1301
1474
  console.log(ui.divider());
1302
1475
  }
1303
1476
  first = false;
1304
- console.log(ui.kv("Name", chalk5.bold(spec.name)));
1477
+ console.log(ui.kv("Name", chalk6.bold(spec.name)));
1305
1478
  console.log(ui.kv("Description", spec.description));
1306
1479
  console.log(ui.kv("Date", `${date} \xB7 ${toolCount} tools`));
1307
- console.log(ui.kv("ID", chalk5.dim(spec.id)));
1480
+ console.log(ui.kv("ID", chalk6.dim(spec.id)));
1308
1481
  console.log("");
1309
1482
  } catch {
1310
1483
  }
@@ -1313,9 +1486,9 @@ var listCommand = new Command3("list").description("Show saved environments").ac
1313
1486
 
1314
1487
  // src/commands/activate.ts
1315
1488
  import { Command as Command4 } from "commander";
1316
- import chalk6 from "chalk";
1317
- import fs8 from "fs/promises";
1318
- import path8 from "path";
1489
+ import chalk7 from "chalk";
1490
+ import fs9 from "fs/promises";
1491
+ import path9 from "path";
1319
1492
  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) => {
1320
1493
  printCompactBanner();
1321
1494
  const envsDir = getEnvsDir();
@@ -1325,7 +1498,7 @@ var activateCommand = new Command4("activate").description("Re-deploy a saved en
1325
1498
  let fromTemplate = false;
1326
1499
  let envFiles = [];
1327
1500
  try {
1328
- envFiles = await fs8.readdir(envsDir);
1501
+ envFiles = await fs9.readdir(envsDir);
1329
1502
  } catch {
1330
1503
  }
1331
1504
  match = envFiles.find(
@@ -1336,7 +1509,7 @@ var activateCommand = new Command4("activate").description("Re-deploy a saved en
1336
1509
  } else {
1337
1510
  let templateFiles = [];
1338
1511
  try {
1339
- templateFiles = await fs8.readdir(templatesDir);
1512
+ templateFiles = await fs9.readdir(templatesDir);
1340
1513
  } catch {
1341
1514
  }
1342
1515
  match = templateFiles.find(
@@ -1347,16 +1520,16 @@ var activateCommand = new Command4("activate").description("Re-deploy a saved en
1347
1520
  fromTemplate = true;
1348
1521
  } else {
1349
1522
  console.log(ui.error(`Environment "${envId}" not found.`));
1350
- console.log(chalk6.dim(" Run kairn list to see saved environments."));
1351
- console.log(chalk6.dim(" Run kairn templates to see available templates.\n"));
1523
+ console.log(chalk7.dim(" Run kairn list to see saved environments."));
1524
+ console.log(chalk7.dim(" Run kairn templates to see available templates.\n"));
1352
1525
  process.exit(1);
1353
1526
  }
1354
1527
  }
1355
- const data = await fs8.readFile(path8.join(sourceDir, match), "utf-8");
1528
+ const data = await fs9.readFile(path9.join(sourceDir, match), "utf-8");
1356
1529
  const spec = JSON.parse(data);
1357
- const label = fromTemplate ? chalk6.dim(" (template)") : "";
1358
- console.log(chalk6.cyan(` Activating: ${spec.name}`) + label);
1359
- console.log(chalk6.dim(` ${spec.description}
1530
+ const label = fromTemplate ? chalk7.dim(" (template)") : "";
1531
+ console.log(chalk7.cyan(` Activating: ${spec.name}`) + label);
1532
+ console.log(chalk7.dim(` ${spec.description}
1360
1533
  `));
1361
1534
  const targetDir = process.cwd();
1362
1535
  const written = await writeEnvironment(spec, targetDir);
@@ -1369,22 +1542,22 @@ var activateCommand = new Command4("activate").description("Re-deploy a saved en
1369
1542
 
1370
1543
  // src/commands/update-registry.ts
1371
1544
  import { Command as Command5 } from "commander";
1372
- import chalk7 from "chalk";
1373
- import fs9 from "fs/promises";
1374
- import path9 from "path";
1545
+ import chalk8 from "chalk";
1546
+ import fs10 from "fs/promises";
1547
+ import path10 from "path";
1375
1548
  import { fileURLToPath as fileURLToPath3 } from "url";
1376
1549
  var REGISTRY_URL = "https://raw.githubusercontent.com/ashtonperlroth/kairn/main/src/registry/tools.json";
1377
1550
  async function getLocalRegistryPath() {
1378
1551
  const __filename3 = fileURLToPath3(import.meta.url);
1379
- const __dirname3 = path9.dirname(__filename3);
1552
+ const __dirname3 = path10.dirname(__filename3);
1380
1553
  const candidates = [
1381
- path9.resolve(__dirname3, "../registry/tools.json"),
1382
- path9.resolve(__dirname3, "../src/registry/tools.json"),
1383
- path9.resolve(__dirname3, "../../src/registry/tools.json")
1554
+ path10.resolve(__dirname3, "../registry/tools.json"),
1555
+ path10.resolve(__dirname3, "../src/registry/tools.json"),
1556
+ path10.resolve(__dirname3, "../../src/registry/tools.json")
1384
1557
  ];
1385
1558
  for (const candidate of candidates) {
1386
1559
  try {
1387
- await fs9.access(candidate);
1560
+ await fs10.access(candidate);
1388
1561
  return candidate;
1389
1562
  } catch {
1390
1563
  continue;
@@ -1395,15 +1568,15 @@ async function getLocalRegistryPath() {
1395
1568
  var updateRegistryCommand = new Command5("update-registry").description("Fetch the latest tool registry from GitHub").option("--url <url>", "Custom registry URL").action(async (options) => {
1396
1569
  printCompactBanner();
1397
1570
  const url = options.url || REGISTRY_URL;
1398
- console.log(chalk7.dim(` Fetching registry from ${url}...`));
1571
+ console.log(chalk8.dim(` Fetching registry from ${url}...`));
1399
1572
  try {
1400
1573
  const response = await fetch(url);
1401
1574
  if (!response.ok) {
1402
1575
  console.log(
1403
1576
  ui.error(`Failed to fetch registry: ${response.status} ${response.statusText}`)
1404
1577
  );
1405
- console.log(chalk7.dim(" The remote registry may not be available yet."));
1406
- console.log(chalk7.dim(" Your local registry is still active.\n"));
1578
+ console.log(chalk8.dim(" The remote registry may not be available yet."));
1579
+ console.log(chalk8.dim(" Your local registry is still active.\n"));
1407
1580
  return;
1408
1581
  }
1409
1582
  const text = await response.text();
@@ -1421,35 +1594,35 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
1421
1594
  const registryPath = await getLocalRegistryPath();
1422
1595
  const backupPath = registryPath + ".bak";
1423
1596
  try {
1424
- await fs9.copyFile(registryPath, backupPath);
1597
+ await fs10.copyFile(registryPath, backupPath);
1425
1598
  } catch {
1426
1599
  }
1427
- await fs9.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
1600
+ await fs10.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
1428
1601
  console.log(ui.success(`Registry updated: ${tools.length} tools`));
1429
- console.log(chalk7.dim(` Saved to: ${registryPath}`));
1430
- console.log(chalk7.dim(` Backup: ${backupPath}
1602
+ console.log(chalk8.dim(` Saved to: ${registryPath}`));
1603
+ console.log(chalk8.dim(` Backup: ${backupPath}
1431
1604
  `));
1432
1605
  } catch (err) {
1433
1606
  const msg = err instanceof Error ? err.message : String(err);
1434
1607
  console.log(ui.error(`Network error: ${msg}`));
1435
- console.log(chalk7.dim(" Your local registry is still active.\n"));
1608
+ console.log(chalk8.dim(" Your local registry is still active.\n"));
1436
1609
  }
1437
1610
  });
1438
1611
 
1439
1612
  // src/commands/optimize.ts
1440
1613
  import { Command as Command6 } from "commander";
1441
1614
  import { confirm as confirm2 } from "@inquirer/prompts";
1442
- import chalk8 from "chalk";
1615
+ import chalk9 from "chalk";
1443
1616
  import ora2 from "ora";
1444
- import fs11 from "fs/promises";
1445
- import path11 from "path";
1617
+ import fs12 from "fs/promises";
1618
+ import path12 from "path";
1446
1619
 
1447
1620
  // src/scanner/scan.ts
1448
- import fs10 from "fs/promises";
1449
- import path10 from "path";
1621
+ import fs11 from "fs/promises";
1622
+ import path11 from "path";
1450
1623
  async function fileExists(p) {
1451
1624
  try {
1452
- await fs10.access(p);
1625
+ await fs11.access(p);
1453
1626
  return true;
1454
1627
  } catch {
1455
1628
  return false;
@@ -1457,7 +1630,7 @@ async function fileExists(p) {
1457
1630
  }
1458
1631
  async function readJsonSafe(p) {
1459
1632
  try {
1460
- const data = await fs10.readFile(p, "utf-8");
1633
+ const data = await fs11.readFile(p, "utf-8");
1461
1634
  return JSON.parse(data);
1462
1635
  } catch {
1463
1636
  return null;
@@ -1465,14 +1638,14 @@ async function readJsonSafe(p) {
1465
1638
  }
1466
1639
  async function readFileSafe(p) {
1467
1640
  try {
1468
- return await fs10.readFile(p, "utf-8");
1641
+ return await fs11.readFile(p, "utf-8");
1469
1642
  } catch {
1470
1643
  return null;
1471
1644
  }
1472
1645
  }
1473
1646
  async function listDirSafe(p) {
1474
1647
  try {
1475
- const entries = await fs10.readdir(p);
1648
+ const entries = await fs11.readdir(p);
1476
1649
  return entries.filter((e) => !e.startsWith("."));
1477
1650
  } catch {
1478
1651
  return [];
@@ -1524,7 +1697,7 @@ function extractEnvKeys(content) {
1524
1697
  return keys;
1525
1698
  }
1526
1699
  async function scanProject(dir) {
1527
- const pkg = await readJsonSafe(path10.join(dir, "package.json"));
1700
+ const pkg = await readJsonSafe(path11.join(dir, "package.json"));
1528
1701
  const deps = pkg?.dependencies ? Object.keys(pkg.dependencies) : [];
1529
1702
  const devDeps = pkg?.devDependencies ? Object.keys(pkg.devDependencies) : [];
1530
1703
  const allDeps = [...deps, ...devDeps];
@@ -1552,19 +1725,19 @@ async function scanProject(dir) {
1552
1725
  const framework = detectFramework(allDeps);
1553
1726
  const typescript = keyFiles.includes("tsconfig.json") || allDeps.includes("typescript");
1554
1727
  const testCommand = scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1' ? scripts.test : null;
1555
- const hasTests = testCommand !== null || await fileExists(path10.join(dir, "tests")) || await fileExists(path10.join(dir, "__tests__")) || await fileExists(path10.join(dir, "test"));
1728
+ const hasTests = testCommand !== null || await fileExists(path11.join(dir, "tests")) || await fileExists(path11.join(dir, "__tests__")) || await fileExists(path11.join(dir, "test"));
1556
1729
  const buildCommand = scripts.build || null;
1557
1730
  const lintCommand = scripts.lint || null;
1558
- const hasSrc = await fileExists(path10.join(dir, "src"));
1559
- const hasDocker = await fileExists(path10.join(dir, "docker-compose.yml")) || await fileExists(path10.join(dir, "Dockerfile"));
1560
- const hasCi = await fileExists(path10.join(dir, ".github/workflows"));
1561
- const hasEnvFile = await fileExists(path10.join(dir, ".env")) || await fileExists(path10.join(dir, ".env.example"));
1731
+ const hasSrc = await fileExists(path11.join(dir, "src"));
1732
+ const hasDocker = await fileExists(path11.join(dir, "docker-compose.yml")) || await fileExists(path11.join(dir, "Dockerfile"));
1733
+ const hasCi = await fileExists(path11.join(dir, ".github/workflows"));
1734
+ const hasEnvFile = await fileExists(path11.join(dir, ".env")) || await fileExists(path11.join(dir, ".env.example"));
1562
1735
  let envKeys = [];
1563
- const envExample = await readFileSafe(path10.join(dir, ".env.example"));
1736
+ const envExample = await readFileSafe(path11.join(dir, ".env.example"));
1564
1737
  if (envExample) {
1565
1738
  envKeys = extractEnvKeys(envExample);
1566
1739
  }
1567
- const claudeDir = path10.join(dir, ".claude");
1740
+ const claudeDir = path11.join(dir, ".claude");
1568
1741
  const hasClaudeDir = await fileExists(claudeDir);
1569
1742
  let existingClaudeMd = null;
1570
1743
  let existingSettings = null;
@@ -1576,21 +1749,21 @@ async function scanProject(dir) {
1576
1749
  let mcpServerCount = 0;
1577
1750
  let claudeMdLineCount = 0;
1578
1751
  if (hasClaudeDir) {
1579
- existingClaudeMd = await readFileSafe(path10.join(claudeDir, "CLAUDE.md"));
1752
+ existingClaudeMd = await readFileSafe(path11.join(claudeDir, "CLAUDE.md"));
1580
1753
  if (existingClaudeMd) {
1581
1754
  claudeMdLineCount = existingClaudeMd.split("\n").length;
1582
1755
  }
1583
- existingSettings = await readJsonSafe(path10.join(claudeDir, "settings.json"));
1584
- existingMcpConfig = await readJsonSafe(path10.join(dir, ".mcp.json"));
1756
+ existingSettings = await readJsonSafe(path11.join(claudeDir, "settings.json"));
1757
+ existingMcpConfig = await readJsonSafe(path11.join(dir, ".mcp.json"));
1585
1758
  if (existingMcpConfig?.mcpServers) {
1586
1759
  mcpServerCount = Object.keys(existingMcpConfig.mcpServers).length;
1587
1760
  }
1588
- existingCommands = (await listDirSafe(path10.join(claudeDir, "commands"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1589
- existingRules = (await listDirSafe(path10.join(claudeDir, "rules"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1590
- existingSkills = await listDirSafe(path10.join(claudeDir, "skills"));
1591
- existingAgents = (await listDirSafe(path10.join(claudeDir, "agents"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1761
+ existingCommands = (await listDirSafe(path11.join(claudeDir, "commands"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1762
+ existingRules = (await listDirSafe(path11.join(claudeDir, "rules"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1763
+ existingSkills = await listDirSafe(path11.join(claudeDir, "skills"));
1764
+ existingAgents = (await listDirSafe(path11.join(claudeDir, "agents"))).filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
1592
1765
  }
1593
- const name = pkg?.name || path10.basename(dir);
1766
+ const name = pkg?.name || path11.basename(dir);
1594
1767
  const description = pkg?.description || "";
1595
1768
  return {
1596
1769
  name,
@@ -1635,31 +1808,31 @@ function simpleDiff(oldContent, newContent) {
1635
1808
  const oldLine = oldLines[i];
1636
1809
  const newLine = newLines[i];
1637
1810
  if (oldLine === void 0) {
1638
- output.push(chalk8.green(`+ ${newLine}`));
1811
+ output.push(chalk9.green(`+ ${newLine}`));
1639
1812
  } else if (newLine === void 0) {
1640
- output.push(chalk8.red(`- ${oldLine}`));
1813
+ output.push(chalk9.red(`- ${oldLine}`));
1641
1814
  } else if (oldLine !== newLine) {
1642
- output.push(chalk8.red(`- ${oldLine}`));
1643
- output.push(chalk8.green(`+ ${newLine}`));
1815
+ output.push(chalk9.red(`- ${oldLine}`));
1816
+ output.push(chalk9.green(`+ ${newLine}`));
1644
1817
  }
1645
1818
  }
1646
1819
  return output;
1647
1820
  }
1648
- async function generateDiff(spec, targetDir) {
1649
- const fileMap = buildFileMap(spec);
1821
+ async function generateDiff(spec, targetDir, options) {
1822
+ const fileMap = buildFileMap(spec, options);
1650
1823
  const results = [];
1651
1824
  for (const [relativePath, newContent] of fileMap) {
1652
- const absolutePath = path11.join(targetDir, relativePath);
1825
+ const absolutePath = path12.join(targetDir, relativePath);
1653
1826
  let oldContent = null;
1654
1827
  try {
1655
- oldContent = await fs11.readFile(absolutePath, "utf-8");
1828
+ oldContent = await fs12.readFile(absolutePath, "utf-8");
1656
1829
  } catch {
1657
1830
  }
1658
1831
  if (oldContent === null) {
1659
1832
  results.push({
1660
1833
  path: relativePath,
1661
1834
  status: "new",
1662
- diff: chalk8.green("+ NEW FILE")
1835
+ diff: chalk9.green("+ NEW FILE")
1663
1836
  });
1664
1837
  } else if (oldContent === newContent) {
1665
1838
  results.push({
@@ -1805,7 +1978,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1805
1978
  console.log(ui.success("No obvious issues found"));
1806
1979
  }
1807
1980
  if (options.auditOnly) {
1808
- console.log(chalk8.dim("\n Audit complete. Run without --audit-only to generate optimized environment.\n"));
1981
+ console.log(chalk9.dim("\n Audit complete. Run without --audit-only to generate optimized environment.\n"));
1809
1982
  return;
1810
1983
  }
1811
1984
  if (!options.yes) {
@@ -1815,19 +1988,19 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1815
1988
  default: false
1816
1989
  });
1817
1990
  if (!proceed) {
1818
- console.log(chalk8.dim("\n Aborted.\n"));
1991
+ console.log(chalk9.dim("\n Aborted.\n"));
1819
1992
  return;
1820
1993
  }
1821
1994
  }
1822
1995
  } else {
1823
- console.log(chalk8.dim("\n No existing .claude/ directory found \u2014 generating from scratch.\n"));
1996
+ console.log(chalk9.dim("\n No existing .claude/ directory found \u2014 generating from scratch.\n"));
1824
1997
  if (!options.yes) {
1825
1998
  const proceed = await confirm2({
1826
1999
  message: "Generate Claude Code environment for this project?",
1827
2000
  default: true
1828
2001
  });
1829
2002
  if (!proceed) {
1830
- console.log(chalk8.dim("\n Aborted.\n"));
2003
+ console.log(chalk9.dim("\n Aborted.\n"));
1831
2004
  return;
1832
2005
  }
1833
2006
  }
@@ -1863,8 +2036,9 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1863
2036
  console.log(ui.tool(name, tool.reason));
1864
2037
  }
1865
2038
  }
2039
+ const hasEnvVars = summary.envSetup.length > 0;
1866
2040
  if (options.diff) {
1867
- const diffs = await generateDiff(spec, targetDir);
2041
+ const diffs = await generateDiff(spec, targetDir, { hasEnvVars });
1868
2042
  const changedDiffs = diffs.filter((d) => d.status !== "unchanged");
1869
2043
  if (changedDiffs.length === 0) {
1870
2044
  console.log(ui.success("No changes needed \u2014 environment is already up to date."));
@@ -1873,7 +2047,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1873
2047
  }
1874
2048
  console.log(ui.section("Changes Preview"));
1875
2049
  for (const d of changedDiffs) {
1876
- console.log(chalk8.cyan(`
2050
+ console.log(chalk9.cyan(`
1877
2051
  --- ${d.path}`));
1878
2052
  if (d.status === "new") {
1879
2053
  console.log(` ${d.diff}`);
@@ -1889,7 +2063,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1889
2063
  default: true
1890
2064
  });
1891
2065
  if (!apply) {
1892
- console.log(chalk8.dim("\n Aborted.\n"));
2066
+ console.log(chalk9.dim("\n Aborted.\n"));
1893
2067
  return;
1894
2068
  }
1895
2069
  }
@@ -1900,20 +2074,14 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1900
2074
  console.log(ui.success(`Ready! Run: $ hermes`));
1901
2075
  console.log("");
1902
2076
  } else {
1903
- const written = await writeEnvironment(spec, targetDir);
2077
+ const written = await writeEnvironment(spec, targetDir, { hasEnvVars });
1904
2078
  console.log(ui.section("Files Written"));
1905
2079
  for (const file of written) {
1906
2080
  console.log(ui.file(file));
1907
2081
  }
1908
- if (summary.envSetup.length > 0) {
1909
- console.log(ui.section("Setup Required"));
1910
- const seen = /* @__PURE__ */ new Set();
1911
- for (const env of summary.envSetup) {
1912
- if (seen.has(env.envVar)) continue;
1913
- seen.add(env.envVar);
1914
- console.log(ui.envVar(env.envVar, env.description, env.signupUrl));
1915
- console.log("");
1916
- }
2082
+ if (hasEnvVars) {
2083
+ await collectAndWriteKeys(summary.envSetup, targetDir);
2084
+ console.log("");
1917
2085
  }
1918
2086
  if (summary.pluginCommands.length > 0) {
1919
2087
  console.log(ui.section("Plugins"));
@@ -1930,7 +2098,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
1930
2098
 
1931
2099
  // src/commands/doctor.ts
1932
2100
  import { Command as Command7 } from "commander";
1933
- import chalk9 from "chalk";
2101
+ import chalk10 from "chalk";
1934
2102
  function runChecks(profile) {
1935
2103
  const checks = [];
1936
2104
  if (!profile.existingClaudeMd) {
@@ -2062,12 +2230,12 @@ var doctorCommand = new Command7("doctor").description(
2062
2230
  ).action(async () => {
2063
2231
  printFullBanner("Doctor");
2064
2232
  const targetDir = process.cwd();
2065
- console.log(chalk9.dim(" Checking .claude/ environment...\n"));
2233
+ console.log(chalk10.dim(" Checking .claude/ environment...\n"));
2066
2234
  const profile = await scanProject(targetDir);
2067
2235
  if (!profile.hasClaudeDir) {
2068
2236
  console.log(ui.error("No .claude/ directory found.\n"));
2069
2237
  console.log(
2070
- chalk9.dim(" Run ") + chalk9.bold("kairn describe") + chalk9.dim(" or ") + chalk9.bold("kairn optimize") + chalk9.dim(" to generate one.\n")
2238
+ chalk10.dim(" Run ") + chalk10.bold("kairn describe") + chalk10.dim(" or ") + chalk10.bold("kairn optimize") + chalk10.dim(" to generate one.\n")
2071
2239
  );
2072
2240
  process.exit(1);
2073
2241
  }
@@ -2090,7 +2258,7 @@ var doctorCommand = new Command7("doctor").description(
2090
2258
  return sum;
2091
2259
  }, 0);
2092
2260
  const percentage = Math.round(score / maxScore * 100);
2093
- const scoreColor = percentage >= 80 ? chalk9.green : percentage >= 50 ? chalk9.yellow : chalk9.red;
2261
+ const scoreColor = percentage >= 80 ? chalk10.green : percentage >= 50 ? chalk10.yellow : chalk10.red;
2094
2262
  console.log(
2095
2263
  `
2096
2264
  Score: ${scoreColor(`${score}/${maxScore}`)} (${scoreColor(`${percentage}%`)})
@@ -2098,14 +2266,14 @@ var doctorCommand = new Command7("doctor").description(
2098
2266
  );
2099
2267
  if (percentage < 80) {
2100
2268
  console.log(
2101
- chalk9.dim(" Run ") + chalk9.bold("kairn optimize") + chalk9.dim(" to fix issues.\n")
2269
+ chalk10.dim(" Run ") + chalk10.bold("kairn optimize") + chalk10.dim(" to fix issues.\n")
2102
2270
  );
2103
2271
  }
2104
2272
  });
2105
2273
 
2106
2274
  // src/commands/registry.ts
2107
2275
  import { Command as Command8 } from "commander";
2108
- import chalk10 from "chalk";
2276
+ import chalk11 from "chalk";
2109
2277
  import { input as input2, select as select2 } from "@inquirer/prompts";
2110
2278
  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) => {
2111
2279
  printCompactBanner();
@@ -2130,7 +2298,7 @@ var listCommand2 = new Command8("list").description("List tools in the registry"
2130
2298
  );
2131
2299
  }
2132
2300
  if (tools.length === 0) {
2133
- console.log(chalk10.dim("\n No tools found.\n"));
2301
+ console.log(chalk11.dim("\n No tools found.\n"));
2134
2302
  return;
2135
2303
  }
2136
2304
  const bundledCount = all.filter((t) => !userIds.has(t.id)).length;
@@ -2144,13 +2312,13 @@ var listCommand2 = new Command8("list").description("List tools in the registry"
2144
2312
  `tier ${tool.tier}`,
2145
2313
  tool.auth
2146
2314
  ].join(", ");
2147
- console.log(` ${ui.accent(tool.id)}` + chalk10.dim(` (${meta})`));
2148
- console.log(chalk10.dim(` ${tool.description}`));
2315
+ console.log(` ${ui.accent(tool.id)}` + chalk11.dim(` (${meta})`));
2316
+ console.log(chalk11.dim(` ${tool.description}`));
2149
2317
  if (tool.best_for.length > 0) {
2150
- console.log(chalk10.dim(` Best for: ${tool.best_for.join(", ")}`));
2318
+ console.log(chalk11.dim(` Best for: ${tool.best_for.join(", ")}`));
2151
2319
  }
2152
2320
  if (isUser) {
2153
- console.log(chalk10.yellow(" [USER-DEFINED]"));
2321
+ console.log(chalk11.yellow(" [USER-DEFINED]"));
2154
2322
  }
2155
2323
  console.log("");
2156
2324
  }
@@ -2158,7 +2326,7 @@ var listCommand2 = new Command8("list").description("List tools in the registry"
2158
2326
  const shownUser = tools.filter((t) => userIds.has(t.id)).length;
2159
2327
  const shownBundled = totalShown - shownUser;
2160
2328
  console.log(
2161
- chalk10.dim(
2329
+ chalk11.dim(
2162
2330
  ` ${totalShown} tool${totalShown !== 1 ? "s" : ""} (${shownBundled} bundled, ${shownUser} user-defined)`
2163
2331
  ) + "\n"
2164
2332
  );
@@ -2282,20 +2450,20 @@ var registryCommand = new Command8("registry").description("Manage the tool regi
2282
2450
 
2283
2451
  // src/commands/templates.ts
2284
2452
  import { Command as Command9 } from "commander";
2285
- import chalk11 from "chalk";
2286
- import fs12 from "fs/promises";
2287
- import path12 from "path";
2453
+ import chalk12 from "chalk";
2454
+ import fs13 from "fs/promises";
2455
+ import path13 from "path";
2288
2456
  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) => {
2289
2457
  printCompactBanner();
2290
2458
  const templatesDir = getTemplatesDir();
2291
2459
  let files;
2292
2460
  try {
2293
- files = await fs12.readdir(templatesDir);
2461
+ files = await fs13.readdir(templatesDir);
2294
2462
  } catch {
2295
2463
  console.log(
2296
- chalk11.dim(
2464
+ chalk12.dim(
2297
2465
  " No templates found. Templates will be installed with "
2298
- ) + chalk11.bold("kairn init") + chalk11.dim(
2466
+ ) + chalk12.bold("kairn init") + chalk12.dim(
2299
2467
  " or you can add .json files to ~/.kairn/templates/\n"
2300
2468
  )
2301
2469
  );
@@ -2304,9 +2472,9 @@ var templatesCommand = new Command9("templates").description("Browse available t
2304
2472
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
2305
2473
  if (jsonFiles.length === 0) {
2306
2474
  console.log(
2307
- chalk11.dim(
2475
+ chalk12.dim(
2308
2476
  " No templates found. Templates will be installed with "
2309
- ) + chalk11.bold("kairn init") + chalk11.dim(
2477
+ ) + chalk12.bold("kairn init") + chalk12.dim(
2310
2478
  " or you can add .json files to ~/.kairn/templates/\n"
2311
2479
  )
2312
2480
  );
@@ -2315,8 +2483,8 @@ var templatesCommand = new Command9("templates").description("Browse available t
2315
2483
  const templates = [];
2316
2484
  for (const file of jsonFiles) {
2317
2485
  try {
2318
- const data = await fs12.readFile(
2319
- path12.join(templatesDir, file),
2486
+ const data = await fs13.readFile(
2487
+ path13.join(templatesDir, file),
2320
2488
  "utf-8"
2321
2489
  );
2322
2490
  const spec = JSON.parse(data);
@@ -2334,7 +2502,7 @@ var templatesCommand = new Command9("templates").description("Browse available t
2334
2502
  }
2335
2503
  if (filtered.length === 0) {
2336
2504
  console.log(
2337
- chalk11.dim(` No templates matched category "${options.category}".
2505
+ chalk12.dim(` No templates matched category "${options.category}".
2338
2506
  `)
2339
2507
  );
2340
2508
  return;
@@ -2345,8 +2513,8 @@ var templatesCommand = new Command9("templates").description("Browse available t
2345
2513
  const toolCount = spec.tools?.length ?? 0;
2346
2514
  const commandCount = Object.keys(spec.harness?.commands ?? {}).length;
2347
2515
  const ruleCount = Object.keys(spec.harness?.rules ?? {}).length;
2348
- console.log(ui.kv("Name", chalk11.bold(spec.name)));
2349
- console.log(ui.kv("ID", chalk11.dim(spec.id)));
2516
+ console.log(ui.kv("Name", chalk12.bold(spec.name)));
2517
+ console.log(ui.kv("ID", chalk12.dim(spec.id)));
2350
2518
  console.log(ui.kv("Description", spec.description));
2351
2519
  console.log(
2352
2520
  ui.kv("Contents", `${toolCount} tools \xB7 ${commandCount} commands \xB7 ${ruleCount} rules`)
@@ -2354,16 +2522,139 @@ var templatesCommand = new Command9("templates").description("Browse available t
2354
2522
  console.log("");
2355
2523
  }
2356
2524
  console.log(
2357
- chalk11.dim(` ${filtered.length} template${filtered.length === 1 ? "" : "s"} available
2525
+ chalk12.dim(` ${filtered.length} template${filtered.length === 1 ? "" : "s"} available
2358
2526
  `)
2359
2527
  );
2360
2528
  });
2361
2529
 
2530
+ // src/commands/keys.ts
2531
+ import { Command as Command10 } from "commander";
2532
+ import { password as password3 } from "@inquirer/prompts";
2533
+ import chalk13 from "chalk";
2534
+ import fs14 from "fs/promises";
2535
+ import path14 from "path";
2536
+ var keysCommand = new Command10("keys").description("Add or update API keys for the current environment").option("--show", "Show which keys are set vs missing").action(async (options) => {
2537
+ printCompactBanner();
2538
+ const targetDir = process.cwd();
2539
+ const requiredVars = await detectRequiredEnvVars(targetDir);
2540
+ if (requiredVars.length === 0) {
2541
+ console.log(
2542
+ ui.info("No MCP servers found in .mcp.json \u2014 no API keys needed.")
2543
+ );
2544
+ console.log("");
2545
+ return;
2546
+ }
2547
+ const existing = await readEnvFile(targetDir);
2548
+ const registry = await loadRegistry();
2549
+ const envSetupMap = /* @__PURE__ */ new Map();
2550
+ for (const tool of registry) {
2551
+ if (!tool.env_vars) continue;
2552
+ for (const ev of tool.env_vars) {
2553
+ if (requiredVars.includes(ev.name)) {
2554
+ envSetupMap.set(ev.name, {
2555
+ toolName: tool.name,
2556
+ envVar: ev.name,
2557
+ description: ev.description,
2558
+ signupUrl: tool.signup_url
2559
+ });
2560
+ }
2561
+ }
2562
+ }
2563
+ for (const varName of requiredVars) {
2564
+ if (!envSetupMap.has(varName)) {
2565
+ envSetupMap.set(varName, {
2566
+ toolName: "unknown",
2567
+ envVar: varName,
2568
+ description: "Required by MCP server"
2569
+ });
2570
+ }
2571
+ }
2572
+ if (options.show) {
2573
+ console.log(ui.section("API Key Status"));
2574
+ console.log("");
2575
+ for (const varName of requiredVars) {
2576
+ const value = existing.get(varName);
2577
+ const info = envSetupMap.get(varName);
2578
+ const toolLabel = info?.toolName !== "unknown" ? chalk13.dim(` (${info?.toolName})`) : "";
2579
+ if (value && value.length > 0) {
2580
+ const masked = value.slice(0, 4) + "\u2022".repeat(Math.max(0, value.length - 4));
2581
+ console.log(chalk13.green(` \u2713 ${varName}`) + toolLabel + chalk13.dim(` = ${masked}`));
2582
+ } else {
2583
+ console.log(chalk13.yellow(` \u2717 ${varName}`) + toolLabel + chalk13.dim(" = (not set)"));
2584
+ if (info?.signupUrl) {
2585
+ console.log(chalk13.dim(` Get one at: ${info.signupUrl}`));
2586
+ }
2587
+ }
2588
+ }
2589
+ const setCount = requiredVars.filter((v) => {
2590
+ const val = existing.get(v);
2591
+ return val && val.length > 0;
2592
+ }).length;
2593
+ const missingCount = requiredVars.length - setCount;
2594
+ console.log("");
2595
+ if (missingCount === 0) {
2596
+ console.log(ui.success(`All ${setCount} key(s) configured`));
2597
+ } else {
2598
+ console.log(
2599
+ ui.warn(`${missingCount} key(s) missing \u2014 run ${chalk13.bold("kairn keys")} to add them`)
2600
+ );
2601
+ }
2602
+ console.log("");
2603
+ return;
2604
+ }
2605
+ const missing = requiredVars.filter((v) => {
2606
+ const val = existing.get(v);
2607
+ return !val || val.length === 0;
2608
+ });
2609
+ if (missing.length === 0) {
2610
+ console.log(ui.success("All API keys are already configured."));
2611
+ console.log(chalk13.dim(" Use --show to see current keys.\n"));
2612
+ return;
2613
+ }
2614
+ console.log(ui.section("API Keys"));
2615
+ console.log(chalk13.dim(` ${missing.length} key(s) need to be set. Press Enter to skip.
2616
+ `));
2617
+ const envEntries = new Map(existing);
2618
+ let keysEntered = 0;
2619
+ for (const varName of missing) {
2620
+ const info = envSetupMap.get(varName);
2621
+ console.log(
2622
+ chalk13.bold(` ${varName}`) + (info?.toolName !== "unknown" ? chalk13.dim(` (${info?.toolName})`) : "")
2623
+ );
2624
+ if (info?.signupUrl) {
2625
+ console.log(chalk13.dim(` Get one at: ${info.signupUrl}`));
2626
+ }
2627
+ const value = await password3({
2628
+ message: varName,
2629
+ mask: "\u2022"
2630
+ });
2631
+ if (value && value.trim()) {
2632
+ envEntries.set(varName, value.trim());
2633
+ console.log(chalk13.green(" \u2713 saved\n"));
2634
+ keysEntered++;
2635
+ } else {
2636
+ console.log(chalk13.dim(" (skipped)\n"));
2637
+ }
2638
+ }
2639
+ const envLines = [
2640
+ "# Generated by Kairn \u2014 API keys for MCP servers",
2641
+ "# Do NOT commit this file to git",
2642
+ ""
2643
+ ];
2644
+ for (const [key, value] of envEntries) {
2645
+ envLines.push(`${key}=${value}`);
2646
+ }
2647
+ const envPath = path14.join(targetDir, ".env");
2648
+ await fs14.writeFile(envPath, envLines.join("\n") + "\n", "utf-8");
2649
+ console.log(chalk13.green(` \u2713 ${keysEntered} key(s) saved to .env`));
2650
+ console.log("");
2651
+ });
2652
+
2362
2653
  // src/cli.ts
2363
- var program = new Command10();
2654
+ var program = new Command11();
2364
2655
  program.name("kairn").description(
2365
2656
  "Compile natural language intent into optimized Claude Code environments"
2366
- ).version("1.6.0").option("--no-color", "Disable colored output");
2657
+ ).version("1.8.0").option("--no-color", "Disable colored output");
2367
2658
  program.addCommand(initCommand);
2368
2659
  program.addCommand(describeCommand);
2369
2660
  program.addCommand(optimizeCommand);
@@ -2373,8 +2664,9 @@ program.addCommand(updateRegistryCommand);
2373
2664
  program.addCommand(doctorCommand);
2374
2665
  program.addCommand(registryCommand);
2375
2666
  program.addCommand(templatesCommand);
2667
+ program.addCommand(keysCommand);
2376
2668
  if (process.argv.includes("--no-color") || process.env.NO_COLOR) {
2377
- chalk12.level = 0;
2669
+ chalk14.level = 0;
2378
2670
  }
2379
2671
  program.parse();
2380
2672
  //# sourceMappingURL=cli.js.map