kairn-cli 1.11.0 → 1.13.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
@@ -256,6 +256,113 @@ var ui = {
256
256
  `);
257
257
  }
258
258
  };
259
+ function formatTime(seconds) {
260
+ if (seconds < 60) return `${seconds}s`;
261
+ const min = Math.floor(seconds / 60);
262
+ const sec = seconds % 60;
263
+ return sec > 0 ? `${min}m ${sec}s` : `${min} min`;
264
+ }
265
+ function estimateTime(model, intent) {
266
+ const wordCount = intent.split(/\s+/).length;
267
+ const isComplex = wordCount > 40;
268
+ const perPass = {
269
+ "haiku": 5,
270
+ "sonnet": 20,
271
+ "opus": 60,
272
+ "gpt-4.1-mini": 10,
273
+ "gpt-4.1": 25,
274
+ "gpt-5": 15,
275
+ "o4-mini": 12,
276
+ "gemini-2.5-flash": 8,
277
+ "gemini-3-flash": 8,
278
+ "gemini-2.5-pro": 30,
279
+ "gemini-3.1-pro": 30,
280
+ "grok-4.1-fast": 10,
281
+ "grok-4.20": 25,
282
+ "deepseek": 15,
283
+ "mistral-large": 20,
284
+ "codestral": 15,
285
+ "mistral-small": 10,
286
+ "llama": 10,
287
+ "qwen": 10
288
+ };
289
+ const basePerPass = Object.entries(perPass).find(([k]) => model.toLowerCase().includes(k))?.[1] ?? 20;
290
+ const totalBase = basePerPass * 2;
291
+ if (isComplex) {
292
+ const low = Math.floor(totalBase * 1.5);
293
+ const high = Math.floor(totalBase * 4);
294
+ return `~${formatTime(low)}-${formatTime(high)} (complex workflow)`;
295
+ }
296
+ return `~${formatTime(totalBase)}`;
297
+ }
298
+ function createProgressRenderer() {
299
+ const lines = [];
300
+ let intervalId = null;
301
+ let currentPhase = "";
302
+ let phaseStart = Date.now();
303
+ let lineCount = 0;
304
+ function render() {
305
+ if (lineCount > 0) {
306
+ process.stdout.write(`\x1B[${lineCount}A`);
307
+ }
308
+ for (const line of lines) {
309
+ process.stdout.write("\x1B[2K" + line + "\n");
310
+ }
311
+ lineCount = lines.length;
312
+ }
313
+ function updateElapsed() {
314
+ if (!currentPhase) return;
315
+ const elapsed = Math.floor((Date.now() - phaseStart) / 1e3);
316
+ const lastIdx = lines.length - 1;
317
+ if (lastIdx >= 0) {
318
+ lines[lastIdx] = lines[lastIdx].replace(/\[\d+s\]/, `[${elapsed}s]`);
319
+ render();
320
+ }
321
+ }
322
+ return {
323
+ update(progress) {
324
+ if (progress.status === "running") {
325
+ currentPhase = progress.phase;
326
+ phaseStart = Date.now();
327
+ lines.push(` ${warmStone("\u25D0")} ${progress.message} ${chalk.dim("[0s]")}`);
328
+ if (!intervalId) {
329
+ intervalId = setInterval(updateElapsed, 1e3);
330
+ }
331
+ } else if (progress.status === "success") {
332
+ const lastIdx = lines.length - 1;
333
+ const elapsed = progress.elapsed != null ? ` ${chalk.dim("\u2014")} ${chalk.dim(Math.floor(progress.elapsed) + "s")}` : "";
334
+ const detail = progress.detail ? ` ${chalk.dim("(" + progress.detail + ")")}` : "";
335
+ if (lastIdx >= 0) {
336
+ lines[lastIdx] = ` ${chalk.green("\u2714")} ${progress.message}${detail}${elapsed}`;
337
+ }
338
+ currentPhase = "";
339
+ } else if (progress.status === "warning") {
340
+ const lastIdx = lines.length - 1;
341
+ if (lastIdx >= 0) {
342
+ lines[lastIdx] = ` ${chalk.yellow("\u26A0")} ${progress.message}`;
343
+ }
344
+ currentPhase = progress.phase;
345
+ phaseStart = Date.now();
346
+ lines.push(` ${warmStone("\u25D0")} Retrying in concise mode... ${chalk.dim("[0s]")}`);
347
+ }
348
+ render();
349
+ },
350
+ finish() {
351
+ if (intervalId) clearInterval(intervalId);
352
+ currentPhase = "";
353
+ render();
354
+ },
355
+ fail(err) {
356
+ if (intervalId) clearInterval(intervalId);
357
+ currentPhase = "";
358
+ const lastIdx = lines.length - 1;
359
+ if (lastIdx >= 0) {
360
+ lines[lastIdx] = ` ${chalk.red("\u2716")} Compilation failed`;
361
+ }
362
+ render();
363
+ }
364
+ };
365
+ }
259
366
 
260
367
  // src/logo.ts
261
368
  import chalk2 from "chalk";
@@ -450,7 +557,6 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
450
557
  import { Command as Command2 } from "commander";
451
558
  import { input as input2, confirm, select as select2 } from "@inquirer/prompts";
452
559
  import chalk5 from "chalk";
453
- import ora from "ora";
454
560
 
455
561
  // src/compiler/compile.ts
456
562
  import fs4 from "fs/promises";
@@ -600,6 +706,16 @@ Use subagents for deep investigation to keep main context clean.
600
706
  - Do not create abstractions for one-time operations
601
707
  - Complete the task fully \u2014 don't gold-plate, but don't leave it half-done
602
708
  - Prefer editing existing files over creating new ones
709
+
710
+ ## First Turn Protocol
711
+
712
+ At the start of every session, before doing ANY work:
713
+ 1. Run \`pwd && ls -la && git status --short\` to orient yourself
714
+ 2. Check relevant runtimes (e.g. \`node --version\`, \`python3 --version\` \u2014 pick what fits this project)
715
+ 3. Read any task-tracking files (docs/SPRINT.md, docs/DECISIONS.md)
716
+ 4. Summarize what you see in 2-3 lines, then proceed
717
+
718
+ This saves 2-5 exploratory turns. Never ask "what files are here?" \u2014 look first.
603
719
  \`\`\`
604
720
 
605
721
  Do not add generic filler. Every line must be specific to the user's workflow.
@@ -621,6 +737,7 @@ Do not add generic filler. Every line must be specific to the user's workflow.
621
737
  13. A "Debugging" section in CLAUDE.md (2 lines: paste raw errors, use subagents)
622
738
  14. A "Git Workflow" section in CLAUDE.md (3 rules: small commits, conventional format, <200 lines PR)
623
739
  15. "Engineering Standards", "Tool Usage Policy", and "Code Philosophy" sections in CLAUDE.md
740
+ 16. A "First Turn Protocol" section in CLAUDE.md (orient before working: pwd, ls, git status, check relevant runtimes, read task files)
624
741
 
625
742
  ## Shell-Integrated Commands
626
743
 
@@ -896,6 +1013,16 @@ Use subagents for deep investigation to keep main context clean.
896
1013
  - Do not create abstractions for one-time operations
897
1014
  - Complete the task fully \u2014 don't gold-plate, but don't leave it half-done
898
1015
  - Prefer editing existing files over creating new ones
1016
+
1017
+ ## First Turn Protocol
1018
+
1019
+ At the start of every session, before doing ANY work:
1020
+ 1. Run \`pwd && ls -la && git status --short\` to orient yourself
1021
+ 2. Check relevant runtimes (e.g. \`node --version\`, \`python3 --version\` \u2014 pick what fits this project)
1022
+ 3. Read any task-tracking files (docs/SPRINT.md, docs/DECISIONS.md)
1023
+ 4. Summarize what you see in 2-3 lines, then proceed
1024
+
1025
+ This saves 2-5 exploratory turns. Never ask "what files are here?" \u2014 look first.
899
1026
  \`\`\`
900
1027
 
901
1028
  Do not add generic filler. Every line must be specific to the user's workflow.
@@ -917,6 +1044,7 @@ Do not add generic filler. Every line must be specific to the user's workflow.
917
1044
  13. A "Debugging" section in CLAUDE.md (2 lines: paste raw errors, use subagents)
918
1045
  14. A "Git Workflow" section in CLAUDE.md (3 rules: small commits, conventional format, <200 lines PR)
919
1046
  15. "Engineering Standards", "Tool Usage Policy", and "Code Philosophy" sections in CLAUDE.md
1047
+ 16. A "First Turn Protocol" section in CLAUDE.md (orient before working: pwd, ls, git status, check relevant runtimes, read task files)
920
1048
 
921
1049
  ## Tool Selection Rules
922
1050
 
@@ -1264,7 +1392,7 @@ function buildMcpConfig(skeleton, registry) {
1264
1392
  }
1265
1393
  return config;
1266
1394
  }
1267
- function validateSpec(spec, onProgress) {
1395
+ function validateSpec(spec) {
1268
1396
  const warnings = [];
1269
1397
  if (spec.tools.length > 8) {
1270
1398
  warnings.push(`${spec.tools.length} MCP servers selected (recommended: \u22646)`);
@@ -1278,25 +1406,33 @@ function validateSpec(spec, onProgress) {
1278
1406
  if (spec.harness.skills && Object.keys(spec.harness.skills).length > 5) {
1279
1407
  warnings.push(`${Object.keys(spec.harness.skills).length} skills (recommended: \u22643)`);
1280
1408
  }
1281
- for (const warning of warnings) {
1282
- onProgress?.(`\u26A0 ${warning}`);
1283
- }
1409
+ return warnings;
1284
1410
  }
1285
1411
  async function compile(intent, onProgress) {
1412
+ const startTime = Date.now();
1286
1413
  const config = await loadConfig();
1287
1414
  if (!config) {
1288
1415
  throw new Error("No config found. Run `kairn init` first.");
1289
1416
  }
1290
- onProgress?.("Loading tool registry...");
1417
+ onProgress?.({ phase: "registry", status: "running", message: "Loading tool registry..." });
1291
1418
  const registry = await loadRegistry();
1292
- onProgress?.("Analyzing workflow...");
1419
+ onProgress?.({ phase: "registry", status: "success", message: "Tool registry loaded", detail: `${registry.length} tools` });
1420
+ onProgress?.({ phase: "pass1", status: "running", message: "Pass 1: Analyzing workflow & selecting tools..." });
1293
1421
  const skeletonMsg = buildSkeletonMessage(intent, registry);
1294
1422
  const skeletonText = await callLLM(config, skeletonMsg, {
1295
1423
  maxTokens: 2048,
1296
1424
  systemPrompt: SKELETON_PROMPT
1297
1425
  });
1298
1426
  const skeleton = parseSkeletonResponse(skeletonText);
1299
- onProgress?.("Generating environment...");
1427
+ const toolNames = skeleton.tools.map((t) => t.tool_id).join(", ");
1428
+ onProgress?.({
1429
+ phase: "pass1",
1430
+ status: "success",
1431
+ message: `Pass 1: Selected ${skeleton.tools.length} tools`,
1432
+ detail: toolNames,
1433
+ elapsed: (Date.now() - startTime) / 1e3
1434
+ });
1435
+ onProgress?.({ phase: "pass2", status: "running", message: "Pass 2: Generating CLAUDE.md, commands, agents..." });
1300
1436
  const harnessMsg = buildHarnessMessage(intent, skeleton);
1301
1437
  let harness;
1302
1438
  try {
@@ -1306,7 +1442,7 @@ async function compile(intent, onProgress) {
1306
1442
  });
1307
1443
  harness = parseHarnessResponse(harnessText);
1308
1444
  } catch {
1309
- onProgress?.("Retrying with concise mode...");
1445
+ onProgress?.({ phase: "pass2-retry", status: "warning", message: "Pass 2: Response too large, retrying in concise mode..." });
1310
1446
  const retryMsg = buildHarnessMessage(intent, skeleton, true);
1311
1447
  const retryText = await callLLM(config, retryMsg, {
1312
1448
  maxTokens: 8192,
@@ -1314,9 +1450,19 @@ async function compile(intent, onProgress) {
1314
1450
  });
1315
1451
  harness = parseHarnessResponse(retryText);
1316
1452
  }
1317
- onProgress?.("Configuring tools...");
1453
+ const cmdCount = Object.keys(harness.commands).length;
1454
+ const agentCount = Object.keys(harness.agents ?? {}).length;
1455
+ const ruleCount = Object.keys(harness.rules).length;
1456
+ onProgress?.({
1457
+ phase: "pass2",
1458
+ status: "success",
1459
+ message: `Pass 2: Generated ${cmdCount} commands, ${agentCount} agents, ${ruleCount} rules`,
1460
+ elapsed: (Date.now() - startTime) / 1e3
1461
+ });
1462
+ onProgress?.({ phase: "pass3", status: "running", message: "Pass 3: Configuring MCP servers & settings..." });
1318
1463
  const settings = buildSettings(skeleton, registry);
1319
1464
  const mcpConfig = buildMcpConfig(skeleton, registry);
1465
+ onProgress?.({ phase: "pass3", status: "success", message: "Pass 3: Configured MCP servers & settings" });
1320
1466
  const spec = {
1321
1467
  id: `env_${crypto.randomUUID()}`,
1322
1468
  intent,
@@ -1336,7 +1482,12 @@ async function compile(intent, onProgress) {
1336
1482
  docs: harness.docs
1337
1483
  }
1338
1484
  };
1339
- validateSpec(spec, onProgress);
1485
+ const warnings = validateSpec(spec);
1486
+ for (const w of warnings) {
1487
+ onProgress?.({ phase: "done", status: "warning", message: `\u26A0 ${w}` });
1488
+ }
1489
+ const totalElapsed = ((Date.now() - startTime) / 1e3).toFixed(0);
1490
+ onProgress?.({ phase: "done", status: "success", message: `Environment compiled in ${totalElapsed}s`, elapsed: (Date.now() - startTime) / 1e3 });
1340
1491
  await ensureDirs();
1341
1492
  const envPath = path4.join(getEnvsDir(), `${spec.id}.json`);
1342
1493
  await fs4.writeFile(envPath, JSON.stringify(spec, null, 2), "utf-8");
@@ -1448,6 +1599,61 @@ ${agentList}
1448
1599
  Type \`/project:help\` in Claude Code for a quick reference.
1449
1600
  `;
1450
1601
  }
1602
+ var BOOTSTRAP_COMMAND = `# Environment Snapshot
1603
+
1604
+ Run this command at the start of any session to gather runtime context.
1605
+ This saves 2-5 exploratory turns.
1606
+
1607
+ 1. Run the following compound command and read the output:
1608
+ \`\`\`bash
1609
+ echo '=== WORKING DIRECTORY ===' && pwd && \\
1610
+ echo '=== PROJECT FILES ===' && ls -la && \\
1611
+ echo '=== GIT STATUS ===' && (git status --short 2>/dev/null || echo 'not a git repo') && \\
1612
+ echo '=== LANGUAGES ===' && \\
1613
+ (node --version 2>&1 || true) && \\
1614
+ (python3 --version 2>&1 || true) && \\
1615
+ (go version 2>&1 || true) && \\
1616
+ (rustc --version 2>&1 || true) && \\
1617
+ echo '=== PACKAGE MANAGERS ===' && \\
1618
+ (npm --version 2>&1 && echo "npm $(npm --version 2>&1)" || true) && \\
1619
+ (pip3 --version 2>&1 || true) && \\
1620
+ (cargo --version 2>&1 || true) && \\
1621
+ echo '=== ENVIRONMENT ===' && \\
1622
+ (cat .env 2>/dev/null | sed 's/=.*/=***/' || echo 'no .env file')
1623
+ \`\`\`
1624
+
1625
+ 2. Summarize the environment in 3-4 lines:
1626
+ - Runtime: [languages + versions found]
1627
+ - Project: [framework, key deps, file count]
1628
+ - State: [git branch, clean/dirty, .env present]
1629
+
1630
+ 3. Keep this summary in context for the rest of the session.`;
1631
+ function buildBootstrapHookCommand(spec) {
1632
+ const checks = [
1633
+ "echo '--- Environment Snapshot ---'",
1634
+ "pwd",
1635
+ "ls -la --color=never | head -20",
1636
+ "echo '---'",
1637
+ "git status --short 2>/dev/null || true",
1638
+ "echo '---'"
1639
+ ];
1640
+ const md = (spec.harness.claude_md ?? "").toLowerCase();
1641
+ if (md.includes("node") || md.includes("typescript") || md.includes("javascript") || md.includes("react") || md.includes("next")) {
1642
+ checks.push("node --version 2>&1 || true");
1643
+ checks.push("cat package.json 2>/dev/null | head -5 || true");
1644
+ }
1645
+ if (md.includes("python") || md.includes("django") || md.includes("flask") || md.includes("fastapi")) {
1646
+ checks.push("python3 --version 2>&1 || true");
1647
+ }
1648
+ if (md.includes("rust") || md.includes("cargo")) {
1649
+ checks.push("rustc --version 2>&1 || true");
1650
+ }
1651
+ if (md.includes("go ") || md.includes("golang")) {
1652
+ checks.push("go version 2>&1 || true");
1653
+ }
1654
+ checks.push("cat .env 2>/dev/null | sed 's/=.*/=***/' || true");
1655
+ return checks.join(" && ");
1656
+ }
1451
1657
  var LOOP_COMMAND_CODE = `# Development Loop
1452
1658
 
1453
1659
  Run an assisted development cycle for the next feature.
@@ -1621,6 +1827,9 @@ function applyAutonomyLevel(spec) {
1621
1827
  settings.hooks = hooks;
1622
1828
  }
1623
1829
  if (level >= 2) {
1830
+ if (!("bootstrap" in commands)) {
1831
+ commands.bootstrap = BOOTSTRAP_COMMAND;
1832
+ }
1624
1833
  if (!("loop" in commands)) {
1625
1834
  commands.loop = isResearchProject(spec) ? LOOP_COMMAND_RESEARCH : LOOP_COMMAND_CODE;
1626
1835
  }
@@ -1632,6 +1841,18 @@ function applyAutonomyLevel(spec) {
1632
1841
  if (!("auto" in commands)) {
1633
1842
  commands.auto = AUTO_COMMAND;
1634
1843
  }
1844
+ const hooks = settings.hooks ?? {};
1845
+ const sessionStart = hooks.SessionStart ?? [];
1846
+ const bootstrapHook = {
1847
+ matcher: "",
1848
+ hooks: [{
1849
+ type: "command",
1850
+ command: buildBootstrapHookCommand(spec)
1851
+ }]
1852
+ };
1853
+ sessionStart.push(bootstrapHook);
1854
+ hooks.SessionStart = sessionStart;
1855
+ settings.hooks = hooks;
1635
1856
  }
1636
1857
  if (level >= 4) {
1637
1858
  if (!("autopilot" in commands)) {
@@ -2112,16 +2333,19 @@ ${clarificationLines}`;
2112
2333
  Autonomy level: ${autonomyLevel} (${autonomyLabel(autonomyLevel)})`;
2113
2334
  }
2114
2335
  console.log(ui.section("Compilation"));
2115
- const spinner = ora({ text: "Loading tool registry...", indent: 2 }).start();
2336
+ const estimate = estimateTime(config.model, finalIntent);
2337
+ console.log(chalk5.dim(` Estimated time: ${estimate} (${config.model})`));
2338
+ console.log("");
2339
+ const renderer = createProgressRenderer();
2116
2340
  let spec;
2117
2341
  try {
2118
- spec = await compile(finalIntent, (msg) => {
2119
- spinner.text = msg;
2342
+ spec = await compile(finalIntent, (progress) => {
2343
+ renderer.update(progress);
2120
2344
  });
2121
2345
  spec.autonomy_level = autonomyLevel;
2122
- spinner.succeed("Environment compiled");
2346
+ renderer.finish();
2123
2347
  } catch (err) {
2124
- spinner.fail("Compilation failed");
2348
+ renderer.fail(err);
2125
2349
  const msg = err instanceof Error ? err.message : String(err);
2126
2350
  console.log(chalk5.red(`
2127
2351
  ${msg}
@@ -2366,7 +2590,7 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
2366
2590
  import { Command as Command6 } from "commander";
2367
2591
  import { confirm as confirm2 } from "@inquirer/prompts";
2368
2592
  import chalk9 from "chalk";
2369
- import ora2 from "ora";
2593
+ import ora from "ora";
2370
2594
  import fs12 from "fs/promises";
2371
2595
  import path12 from "path";
2372
2596
 
@@ -2691,7 +2915,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
2691
2915
  }
2692
2916
  const targetDir = process.cwd();
2693
2917
  console.log(ui.section("Project Scan"));
2694
- const scanSpinner = ora2({ text: "Scanning project...", indent: 2 }).start();
2918
+ const scanSpinner = ora({ text: "Scanning project...", indent: 2 }).start();
2695
2919
  const profile = await scanProject(targetDir);
2696
2920
  scanSpinner.stop();
2697
2921
  if (profile.language) console.log(ui.kv("Language:", profile.language));
@@ -2760,10 +2984,10 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
2760
2984
  }
2761
2985
  const intent = buildOptimizeIntent(profile);
2762
2986
  let spec;
2763
- const spinner = ora2({ text: "Compiling optimized environment...", indent: 2 }).start();
2987
+ const spinner = ora({ text: "Compiling optimized environment...", indent: 2 }).start();
2764
2988
  try {
2765
- spec = await compile(intent, (msg) => {
2766
- spinner.text = msg;
2989
+ spec = await compile(intent, (progress) => {
2990
+ spinner.text = progress.message;
2767
2991
  });
2768
2992
  spinner.succeed("Environment compiled");
2769
2993
  } catch (err) {