kairn-cli 1.11.0 → 1.12.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";
@@ -1264,7 +1370,7 @@ function buildMcpConfig(skeleton, registry) {
1264
1370
  }
1265
1371
  return config;
1266
1372
  }
1267
- function validateSpec(spec, onProgress) {
1373
+ function validateSpec(spec) {
1268
1374
  const warnings = [];
1269
1375
  if (spec.tools.length > 8) {
1270
1376
  warnings.push(`${spec.tools.length} MCP servers selected (recommended: \u22646)`);
@@ -1278,25 +1384,33 @@ function validateSpec(spec, onProgress) {
1278
1384
  if (spec.harness.skills && Object.keys(spec.harness.skills).length > 5) {
1279
1385
  warnings.push(`${Object.keys(spec.harness.skills).length} skills (recommended: \u22643)`);
1280
1386
  }
1281
- for (const warning of warnings) {
1282
- onProgress?.(`\u26A0 ${warning}`);
1283
- }
1387
+ return warnings;
1284
1388
  }
1285
1389
  async function compile(intent, onProgress) {
1390
+ const startTime = Date.now();
1286
1391
  const config = await loadConfig();
1287
1392
  if (!config) {
1288
1393
  throw new Error("No config found. Run `kairn init` first.");
1289
1394
  }
1290
- onProgress?.("Loading tool registry...");
1395
+ onProgress?.({ phase: "registry", status: "running", message: "Loading tool registry..." });
1291
1396
  const registry = await loadRegistry();
1292
- onProgress?.("Analyzing workflow...");
1397
+ onProgress?.({ phase: "registry", status: "success", message: "Tool registry loaded", detail: `${registry.length} tools` });
1398
+ onProgress?.({ phase: "pass1", status: "running", message: "Pass 1: Analyzing workflow & selecting tools..." });
1293
1399
  const skeletonMsg = buildSkeletonMessage(intent, registry);
1294
1400
  const skeletonText = await callLLM(config, skeletonMsg, {
1295
1401
  maxTokens: 2048,
1296
1402
  systemPrompt: SKELETON_PROMPT
1297
1403
  });
1298
1404
  const skeleton = parseSkeletonResponse(skeletonText);
1299
- onProgress?.("Generating environment...");
1405
+ const toolNames = skeleton.tools.map((t) => t.tool_id).join(", ");
1406
+ onProgress?.({
1407
+ phase: "pass1",
1408
+ status: "success",
1409
+ message: `Pass 1: Selected ${skeleton.tools.length} tools`,
1410
+ detail: toolNames,
1411
+ elapsed: (Date.now() - startTime) / 1e3
1412
+ });
1413
+ onProgress?.({ phase: "pass2", status: "running", message: "Pass 2: Generating CLAUDE.md, commands, agents..." });
1300
1414
  const harnessMsg = buildHarnessMessage(intent, skeleton);
1301
1415
  let harness;
1302
1416
  try {
@@ -1306,7 +1420,7 @@ async function compile(intent, onProgress) {
1306
1420
  });
1307
1421
  harness = parseHarnessResponse(harnessText);
1308
1422
  } catch {
1309
- onProgress?.("Retrying with concise mode...");
1423
+ onProgress?.({ phase: "pass2-retry", status: "warning", message: "Pass 2: Response too large, retrying in concise mode..." });
1310
1424
  const retryMsg = buildHarnessMessage(intent, skeleton, true);
1311
1425
  const retryText = await callLLM(config, retryMsg, {
1312
1426
  maxTokens: 8192,
@@ -1314,9 +1428,19 @@ async function compile(intent, onProgress) {
1314
1428
  });
1315
1429
  harness = parseHarnessResponse(retryText);
1316
1430
  }
1317
- onProgress?.("Configuring tools...");
1431
+ const cmdCount = Object.keys(harness.commands).length;
1432
+ const agentCount = Object.keys(harness.agents ?? {}).length;
1433
+ const ruleCount = Object.keys(harness.rules).length;
1434
+ onProgress?.({
1435
+ phase: "pass2",
1436
+ status: "success",
1437
+ message: `Pass 2: Generated ${cmdCount} commands, ${agentCount} agents, ${ruleCount} rules`,
1438
+ elapsed: (Date.now() - startTime) / 1e3
1439
+ });
1440
+ onProgress?.({ phase: "pass3", status: "running", message: "Pass 3: Configuring MCP servers & settings..." });
1318
1441
  const settings = buildSettings(skeleton, registry);
1319
1442
  const mcpConfig = buildMcpConfig(skeleton, registry);
1443
+ onProgress?.({ phase: "pass3", status: "success", message: "Pass 3: Configured MCP servers & settings" });
1320
1444
  const spec = {
1321
1445
  id: `env_${crypto.randomUUID()}`,
1322
1446
  intent,
@@ -1336,7 +1460,12 @@ async function compile(intent, onProgress) {
1336
1460
  docs: harness.docs
1337
1461
  }
1338
1462
  };
1339
- validateSpec(spec, onProgress);
1463
+ const warnings = validateSpec(spec);
1464
+ for (const w of warnings) {
1465
+ onProgress?.({ phase: "done", status: "warning", message: `\u26A0 ${w}` });
1466
+ }
1467
+ const totalElapsed = ((Date.now() - startTime) / 1e3).toFixed(0);
1468
+ onProgress?.({ phase: "done", status: "success", message: `Environment compiled in ${totalElapsed}s`, elapsed: (Date.now() - startTime) / 1e3 });
1340
1469
  await ensureDirs();
1341
1470
  const envPath = path4.join(getEnvsDir(), `${spec.id}.json`);
1342
1471
  await fs4.writeFile(envPath, JSON.stringify(spec, null, 2), "utf-8");
@@ -2112,16 +2241,19 @@ ${clarificationLines}`;
2112
2241
  Autonomy level: ${autonomyLevel} (${autonomyLabel(autonomyLevel)})`;
2113
2242
  }
2114
2243
  console.log(ui.section("Compilation"));
2115
- const spinner = ora({ text: "Loading tool registry...", indent: 2 }).start();
2244
+ const estimate = estimateTime(config.model, finalIntent);
2245
+ console.log(chalk5.dim(` Estimated time: ${estimate} (${config.model})`));
2246
+ console.log("");
2247
+ const renderer = createProgressRenderer();
2116
2248
  let spec;
2117
2249
  try {
2118
- spec = await compile(finalIntent, (msg) => {
2119
- spinner.text = msg;
2250
+ spec = await compile(finalIntent, (progress) => {
2251
+ renderer.update(progress);
2120
2252
  });
2121
2253
  spec.autonomy_level = autonomyLevel;
2122
- spinner.succeed("Environment compiled");
2254
+ renderer.finish();
2123
2255
  } catch (err) {
2124
- spinner.fail("Compilation failed");
2256
+ renderer.fail(err);
2125
2257
  const msg = err instanceof Error ? err.message : String(err);
2126
2258
  console.log(chalk5.red(`
2127
2259
  ${msg}
@@ -2366,7 +2498,7 @@ var updateRegistryCommand = new Command5("update-registry").description("Fetch t
2366
2498
  import { Command as Command6 } from "commander";
2367
2499
  import { confirm as confirm2 } from "@inquirer/prompts";
2368
2500
  import chalk9 from "chalk";
2369
- import ora2 from "ora";
2501
+ import ora from "ora";
2370
2502
  import fs12 from "fs/promises";
2371
2503
  import path12 from "path";
2372
2504
 
@@ -2691,7 +2823,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
2691
2823
  }
2692
2824
  const targetDir = process.cwd();
2693
2825
  console.log(ui.section("Project Scan"));
2694
- const scanSpinner = ora2({ text: "Scanning project...", indent: 2 }).start();
2826
+ const scanSpinner = ora({ text: "Scanning project...", indent: 2 }).start();
2695
2827
  const profile = await scanProject(targetDir);
2696
2828
  scanSpinner.stop();
2697
2829
  if (profile.language) console.log(ui.kv("Language:", profile.language));
@@ -2760,10 +2892,10 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
2760
2892
  }
2761
2893
  const intent = buildOptimizeIntent(profile);
2762
2894
  let spec;
2763
- const spinner = ora2({ text: "Compiling optimized environment...", indent: 2 }).start();
2895
+ const spinner = ora({ text: "Compiling optimized environment...", indent: 2 }).start();
2764
2896
  try {
2765
- spec = await compile(intent, (msg) => {
2766
- spinner.text = msg;
2897
+ spec = await compile(intent, (progress) => {
2898
+ spinner.text = progress.message;
2767
2899
  });
2768
2900
  spinner.succeed("Environment compiled");
2769
2901
  } catch (err) {