kairn-cli 1.9.1 → 1.10.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
@@ -4,7 +4,7 @@ import chalk14 from "chalk";
4
4
 
5
5
  // src/commands/init.ts
6
6
  import { Command } from "commander";
7
- import { password, select } from "@inquirer/prompts";
7
+ import { input, password, select } from "@inquirer/prompts";
8
8
  import chalk3 from "chalk";
9
9
  import Anthropic from "@anthropic-ai/sdk";
10
10
  import OpenAI from "openai";
@@ -62,6 +62,114 @@ async function saveConfig(config) {
62
62
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
63
63
  }
64
64
 
65
+ // src/providers.ts
66
+ var PROVIDER_CONFIGS = {
67
+ anthropic: {
68
+ name: "Anthropic",
69
+ verifyModel: "claude-haiku-4-5-20251001",
70
+ cheapModel: "claude-haiku-4-5-20251001"
71
+ },
72
+ openai: {
73
+ name: "OpenAI",
74
+ verifyModel: "gpt-4.1-nano",
75
+ cheapModel: "gpt-4.1-nano"
76
+ },
77
+ google: {
78
+ name: "Google Gemini",
79
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
80
+ verifyModel: "gemini-2.5-flash",
81
+ cheapModel: "gemini-2.5-flash"
82
+ },
83
+ xai: {
84
+ name: "xAI (Grok)",
85
+ baseURL: "https://api.x.ai/v1",
86
+ verifyModel: "grok-4-1-fast-non-reasoning",
87
+ cheapModel: "grok-4-1-fast-non-reasoning"
88
+ },
89
+ deepseek: {
90
+ name: "DeepSeek",
91
+ baseURL: "https://api.deepseek.com",
92
+ verifyModel: "deepseek-chat",
93
+ cheapModel: "deepseek-chat"
94
+ },
95
+ mistral: {
96
+ name: "Mistral",
97
+ baseURL: "https://api.mistral.ai/v1",
98
+ verifyModel: "mistral-small-latest",
99
+ cheapModel: "mistral-small-latest"
100
+ },
101
+ groq: {
102
+ name: "Groq (open-source models)",
103
+ baseURL: "https://api.groq.com/openai/v1",
104
+ verifyModel: "meta-llama/llama-4-scout-17b-16e-instruct",
105
+ cheapModel: "meta-llama/llama-4-scout-17b-16e-instruct"
106
+ }
107
+ };
108
+ var PROVIDER_MODELS = {
109
+ anthropic: [
110
+ { name: "Claude Sonnet 4.6 (recommended)", value: "claude-sonnet-4-6" },
111
+ { name: "Claude Opus 4.6 (highest quality)", value: "claude-opus-4-6" },
112
+ { name: "Claude Haiku 4.5 (fastest, cheapest)", value: "claude-haiku-4-5-20251001" }
113
+ ],
114
+ openai: [
115
+ { name: "GPT-4.1 (recommended \u2014 smartest non-reasoning)", value: "gpt-4.1" },
116
+ { name: "GPT-4.1 mini (faster, cheaper)", value: "gpt-4.1-mini" },
117
+ { name: "o4-mini (reasoning, cost-efficient)", value: "o4-mini" },
118
+ { name: "GPT-5 mini (frontier)", value: "gpt-5-mini" }
119
+ ],
120
+ google: [
121
+ { name: "Gemini 2.5 Flash (recommended \u2014 best value)", value: "gemini-2.5-flash" },
122
+ { name: "Gemini 3 Flash (newest frontier)", value: "gemini-3-flash" },
123
+ { name: "Gemini 2.5 Pro (highest quality)", value: "gemini-2.5-pro" },
124
+ { name: "Gemini 3.1 Pro Preview (most advanced)", value: "gemini-3.1-pro-preview" }
125
+ ],
126
+ xai: [
127
+ { name: "Grok 4.1 Fast (recommended \u2014 $0.20/M, very fast)", value: "grok-4-1-fast-non-reasoning" },
128
+ { name: "Grok 4.20 (frontier quality, 2M context)", value: "grok-4.20-0309-non-reasoning" }
129
+ ],
130
+ deepseek: [
131
+ { name: "DeepSeek V3.2 Chat (recommended \u2014 cheapest good model)", value: "deepseek-chat" },
132
+ { name: "DeepSeek V3.2 Reasoner (with chain-of-thought)", value: "deepseek-reasoner" }
133
+ ],
134
+ mistral: [
135
+ { name: "Mistral Large 3 (recommended \u2014 open-weight flagship)", value: "mistral-large-latest" },
136
+ { name: "Codestral (code-optimized, 256K context)", value: "codestral-latest" },
137
+ { name: "Mistral Small 4 (cheapest)", value: "mistral-small-latest" }
138
+ ],
139
+ groq: [
140
+ { name: "Llama 4 Maverick (recommended \u2014 free, fast)", value: "meta-llama/llama-4-maverick-17b-128e-instruct" },
141
+ { name: "Llama 4 Scout (free, fast)", value: "meta-llama/llama-4-scout-17b-16e-instruct" },
142
+ { name: "DeepSeek R1 70B (free reasoning)", value: "deepseek-r1-distill-llama-70b" },
143
+ { name: "Qwen 3 32B (free, multilingual)", value: "qwen/qwen3-32b" }
144
+ ]
145
+ };
146
+ var PROVIDER_CHOICES = [
147
+ { name: "Anthropic (Claude) \u2014 recommended", value: "anthropic" },
148
+ { name: "OpenAI (GPT)", value: "openai" },
149
+ { name: "Google (Gemini)", value: "google" },
150
+ { name: "xAI (Grok)", value: "xai" },
151
+ { name: "DeepSeek \u2014 cheapest", value: "deepseek" },
152
+ { name: "Mistral \u2014 open-weight", value: "mistral" },
153
+ { name: "Groq \u2014 free tier, open-source models", value: "groq" },
154
+ { name: "Other (OpenAI-compatible endpoint)", value: "other" }
155
+ ];
156
+ function getProviderName(provider) {
157
+ if (provider === "other") return "Custom endpoint";
158
+ return PROVIDER_CONFIGS[provider].name;
159
+ }
160
+ function getBaseURL(provider, customBaseURL) {
161
+ if (provider === "other") return customBaseURL;
162
+ return PROVIDER_CONFIGS[provider]?.baseURL;
163
+ }
164
+ function getCheapModel(provider, fallbackModel) {
165
+ if (provider === "other") return fallbackModel;
166
+ return PROVIDER_CONFIGS[provider].cheapModel;
167
+ }
168
+ function getVerifyModel(provider, fallbackModel) {
169
+ if (provider === "other") return fallbackModel;
170
+ return PROVIDER_CONFIGS[provider].verifyModel;
171
+ }
172
+
65
173
  // src/ui.ts
66
174
  import chalk from "chalk";
67
175
  var maroon = chalk.rgb(139, 0, 0);
@@ -234,62 +342,28 @@ async function installSeedTemplates() {
234
342
  console.log(ui.success(`${installed} template${installed === 1 ? "" : "s"} installed`));
235
343
  }
236
344
  }
237
- var PROVIDER_MODELS = {
238
- anthropic: {
239
- name: "Anthropic",
240
- models: [
241
- { name: "Claude Sonnet 4.6 (recommended \u2014 fast, smart)", value: "claude-sonnet-4-6" },
242
- { name: "Claude Opus 4.6 (highest quality)", value: "claude-opus-4-6" },
243
- { name: "Claude Haiku 4.5 (fastest, cheapest)", value: "claude-haiku-4-5-20251001" }
244
- ]
245
- },
246
- openai: {
247
- name: "OpenAI",
248
- models: [
249
- { name: "GPT-4o (recommended)", value: "gpt-4o" },
250
- { name: "GPT-4o mini (faster, cheaper)", value: "gpt-4o-mini" },
251
- { name: "o3 (reasoning)", value: "o3" }
252
- ]
253
- },
254
- google: {
255
- name: "Google Gemini",
256
- models: [
257
- { name: "Gemini 2.5 Flash (recommended)", value: "gemini-2.5-flash-preview-05-20" },
258
- { name: "Gemini 2.5 Pro (highest quality)", value: "gemini-2.5-pro-preview-05-06" }
259
- ]
260
- }
261
- };
262
- async function verifyKey(provider, apiKey, model) {
345
+ async function verifyKey(provider, apiKey, baseURL, model) {
263
346
  try {
264
347
  if (provider === "anthropic") {
265
- const client = new Anthropic({ apiKey });
266
- await client.messages.create({
267
- model: "claude-haiku-4-5-20251001",
268
- max_tokens: 10,
269
- messages: [{ role: "user", content: "ping" }]
270
- });
271
- return true;
272
- } else if (provider === "openai") {
273
- const client = new OpenAI({ apiKey });
274
- await client.chat.completions.create({
275
- model: "gpt-4o-mini",
276
- max_tokens: 10,
277
- messages: [{ role: "user", content: "ping" }]
278
- });
279
- return true;
280
- } else if (provider === "google") {
281
- const client = new OpenAI({
282
- apiKey,
283
- baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"
284
- });
285
- await client.chat.completions.create({
286
- model: "gemini-2.5-flash-preview-05-20",
348
+ const client2 = new Anthropic({ apiKey });
349
+ await client2.messages.create({
350
+ model: getVerifyModel(provider, model || "claude-haiku-4-5-20251001"),
287
351
  max_tokens: 10,
288
352
  messages: [{ role: "user", content: "ping" }]
289
353
  });
290
354
  return true;
291
355
  }
292
- return false;
356
+ const verifyModel = provider === "other" ? model || "test" : getVerifyModel(provider, model || "");
357
+ const resolvedBaseURL = getBaseURL(provider, baseURL);
358
+ const clientOptions = { apiKey };
359
+ if (resolvedBaseURL) clientOptions.baseURL = resolvedBaseURL;
360
+ const client = new OpenAI(clientOptions);
361
+ await client.chat.completions.create({
362
+ model: verifyModel,
363
+ max_tokens: 10,
364
+ messages: [{ role: "user", content: "ping" }]
365
+ });
366
+ return true;
293
367
  } catch {
294
368
  return false;
295
369
  }
@@ -311,42 +385,52 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
311
385
  }
312
386
  const provider = await select({
313
387
  message: "LLM provider",
314
- choices: [
315
- { name: "Anthropic (Claude) \u2014 recommended", value: "anthropic" },
316
- { name: "OpenAI (GPT)", value: "openai" },
317
- { name: "Google (Gemini)", value: "google" }
318
- ]
319
- });
320
- const providerInfo = PROVIDER_MODELS[provider];
321
- const model = await select({
322
- message: "Compilation model",
323
- choices: providerInfo.models
388
+ choices: PROVIDER_CHOICES
324
389
  });
390
+ let model;
391
+ let baseURL;
392
+ let providerDisplayName;
393
+ if (provider === "other") {
394
+ providerDisplayName = "Custom endpoint";
395
+ baseURL = await input({ message: "Base URL" });
396
+ model = await input({ message: "Model name" });
397
+ } else {
398
+ providerDisplayName = getProviderName(provider);
399
+ model = await select({
400
+ message: "Compilation model",
401
+ choices: PROVIDER_MODELS[provider]
402
+ });
403
+ }
325
404
  const apiKey = await password({
326
- message: `${providerInfo.name} API key`,
405
+ message: `${providerDisplayName} API key${provider === "other" ? " (Enter to skip)" : ""}`,
327
406
  mask: "*"
328
407
  });
329
- if (!apiKey) {
408
+ if (!apiKey && provider !== "other") {
330
409
  console.log(ui.error("No API key provided. Aborting."));
331
410
  process.exit(1);
332
411
  }
333
- console.log(chalk3.dim("\n Verifying API key..."));
334
- const valid = await verifyKey(provider, apiKey, model);
335
- if (!valid) {
336
- console.log(ui.error("Invalid API key. Check your key and try again."));
337
- process.exit(1);
412
+ if (apiKey) {
413
+ console.log(chalk3.dim("\n Verifying API key..."));
414
+ const valid = await verifyKey(provider, apiKey, baseURL, model);
415
+ if (!valid) {
416
+ console.log(ui.error("Invalid API key. Check your key and try again."));
417
+ process.exit(1);
418
+ }
419
+ console.log(ui.success("API key verified"));
420
+ } else {
421
+ console.log(ui.warn("No API key \u2014 skipping verification"));
338
422
  }
339
- console.log(ui.success("API key verified"));
340
423
  const config = {
341
424
  provider,
342
- api_key: apiKey,
425
+ api_key: apiKey || "",
343
426
  model,
427
+ ...baseURL ? { base_url: baseURL } : {},
344
428
  default_runtime: "claude-code",
345
429
  created_at: (/* @__PURE__ */ new Date()).toISOString()
346
430
  };
347
431
  await saveConfig(config);
348
432
  console.log(ui.success(`Config saved to ${chalk3.dim(getConfigPath())}`));
349
- console.log(ui.kv("Provider", providerInfo.name));
433
+ console.log(ui.kv("Provider", providerDisplayName));
350
434
  console.log(ui.kv("Model", model));
351
435
  await installSeedTemplates();
352
436
  const hasClaude = detectClaudeCode();
@@ -364,7 +448,7 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
364
448
 
365
449
  // src/commands/describe.ts
366
450
  import { Command as Command2 } from "commander";
367
- import { input, confirm, select as select2 } from "@inquirer/prompts";
451
+ import { input as input2, confirm, select as select2 } from "@inquirer/prompts";
368
452
  import chalk5 from "chalk";
369
453
  import ora from "ora";
370
454
 
@@ -845,10 +929,11 @@ function classifyError(err, provider) {
845
929
  return `${provider} API error: ${msg}`;
846
930
  }
847
931
  async function callLLM(config, userMessage) {
932
+ const providerName = getProviderName(config.provider);
848
933
  if (config.provider === "anthropic") {
849
- const client = new Anthropic2({ apiKey: config.api_key });
934
+ const client2 = new Anthropic2({ apiKey: config.api_key });
850
935
  try {
851
- const response = await client.messages.create({
936
+ const response = await client2.messages.create({
852
937
  model: config.model,
853
938
  max_tokens: 8192,
854
939
  system: SYSTEM_PROMPT,
@@ -859,35 +944,31 @@ async function callLLM(config, userMessage) {
859
944
  throw new Error("No text response from compiler LLM");
860
945
  }
861
946
  return textBlock.text;
862
- } catch (err) {
863
- throw new Error(classifyError(err, "Anthropic"));
864
- }
865
- } else if (config.provider === "openai" || config.provider === "google") {
866
- const providerName = config.provider === "google" ? "Google" : "OpenAI";
867
- const clientOptions = { apiKey: config.api_key };
868
- if (config.provider === "google") {
869
- clientOptions.baseURL = "https://generativelanguage.googleapis.com/v1beta/openai/";
870
- }
871
- const client = new OpenAI2(clientOptions);
872
- try {
873
- const response = await client.chat.completions.create({
874
- model: config.model,
875
- max_tokens: 8192,
876
- messages: [
877
- { role: "system", content: SYSTEM_PROMPT },
878
- { role: "user", content: userMessage }
879
- ]
880
- });
881
- const text = response.choices[0]?.message?.content;
882
- if (!text) {
883
- throw new Error("No text response from compiler LLM");
884
- }
885
- return text;
886
947
  } catch (err) {
887
948
  throw new Error(classifyError(err, providerName));
888
949
  }
889
950
  }
890
- throw new Error(`Unsupported provider: ${config.provider}. Run \`kairn init\` to reconfigure.`);
951
+ const resolvedBaseURL = getBaseURL(config.provider, config.base_url);
952
+ const clientOptions = { apiKey: config.api_key };
953
+ if (resolvedBaseURL) clientOptions.baseURL = resolvedBaseURL;
954
+ const client = new OpenAI2(clientOptions);
955
+ try {
956
+ const response = await client.chat.completions.create({
957
+ model: config.model,
958
+ max_tokens: 8192,
959
+ messages: [
960
+ { role: "system", content: SYSTEM_PROMPT },
961
+ { role: "user", content: userMessage }
962
+ ]
963
+ });
964
+ const text = response.choices[0]?.message?.content;
965
+ if (!text) {
966
+ throw new Error("No text response from compiler LLM");
967
+ }
968
+ return text;
969
+ } catch (err) {
970
+ throw new Error(classifyError(err, providerName));
971
+ }
891
972
  }
892
973
  function validateSpec(spec, onProgress) {
893
974
  const warnings = [];
@@ -939,9 +1020,7 @@ async function generateClarifications(intent, onProgress) {
939
1020
  }
940
1021
  onProgress?.("Analyzing your request...");
941
1022
  const clarificationConfig = { ...config };
942
- if (config.provider === "anthropic") {
943
- clarificationConfig.model = "claude-haiku-4-5-20251001";
944
- }
1023
+ clarificationConfig.model = getCheapModel(config.provider, config.model);
945
1024
  const response = await callLLM(clarificationConfig, CLARIFICATION_PROMPT + "\n\nUser description: " + intent);
946
1025
  try {
947
1026
  let cleaned = response.trim();
@@ -1653,7 +1732,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
1653
1732
  );
1654
1733
  process.exit(1);
1655
1734
  }
1656
- const intentRaw = intentArg || await input({
1735
+ const intentRaw = intentArg || await input2({
1657
1736
  message: "What do you want your agent to do?"
1658
1737
  });
1659
1738
  if (!intentRaw.trim()) {
@@ -1673,7 +1752,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
1673
1752
  if (clarifications.length > 0) {
1674
1753
  const answers = [];
1675
1754
  for (const c of clarifications) {
1676
- const answer = await input({
1755
+ const answer = await input2({
1677
1756
  message: c.question,
1678
1757
  default: c.suggestion
1679
1758
  });
@@ -2425,7 +2504,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
2425
2504
  console.log(ui.file(file));
2426
2505
  }
2427
2506
  if (hasEnvVars) {
2428
- await collectAndWriteKeys(summary.envSetup, targetDir, { quick: options.quick });
2507
+ await collectAndWriteKeys(summary.envSetup, targetDir);
2429
2508
  console.log("");
2430
2509
  }
2431
2510
  if (summary.pluginCommands.length > 0) {
@@ -2619,7 +2698,7 @@ var doctorCommand = new Command7("doctor").description(
2619
2698
  // src/commands/registry.ts
2620
2699
  import { Command as Command8 } from "commander";
2621
2700
  import chalk11 from "chalk";
2622
- import { input as input2, select as select3 } from "@inquirer/prompts";
2701
+ import { input as input3, select as select3 } from "@inquirer/prompts";
2623
2702
  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) => {
2624
2703
  printCompactBanner();
2625
2704
  let all;
@@ -2679,7 +2758,7 @@ var listCommand2 = new Command8("list").description("List tools in the registry"
2679
2758
  var addCommand = new Command8("add").description("Add a tool to the user registry").action(async () => {
2680
2759
  let id;
2681
2760
  try {
2682
- id = await input2({
2761
+ id = await input3({
2683
2762
  message: "Tool ID (kebab-case)",
2684
2763
  validate: (v) => {
2685
2764
  if (!v) return "ID is required";
@@ -2687,8 +2766,8 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
2687
2766
  return true;
2688
2767
  }
2689
2768
  });
2690
- const name = await input2({ message: "Display name" });
2691
- const description = await input2({ message: "Description" });
2769
+ const name = await input3({ message: "Display name" });
2770
+ const description = await input3({ message: "Description" });
2692
2771
  const category = await select3({
2693
2772
  message: "Category",
2694
2773
  choices: [
@@ -2732,8 +2811,8 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
2732
2811
  if (auth === "api_key" || auth === "connection_string") {
2733
2812
  let addMore = true;
2734
2813
  while (addMore) {
2735
- const varName = await input2({ message: "Env var name" });
2736
- const varDesc = await input2({ message: "Env var description" });
2814
+ const varName = await input3({ message: "Env var name" });
2815
+ const varDesc = await input3({ message: "Env var description" });
2737
2816
  env_vars.push({ name: varName, description: varDesc });
2738
2817
  const another = await select3({
2739
2818
  message: "Add another env var?",
@@ -2745,14 +2824,14 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
2745
2824
  addMore = another;
2746
2825
  }
2747
2826
  }
2748
- const signup_url_raw = await input2({ message: "Signup URL (optional, press enter to skip)" });
2827
+ const signup_url_raw = await input3({ message: "Signup URL (optional, press enter to skip)" });
2749
2828
  const signup_url = signup_url_raw.trim() || void 0;
2750
- const best_for_raw = await input2({ message: "Best-for tags, comma-separated" });
2829
+ const best_for_raw = await input3({ message: "Best-for tags, comma-separated" });
2751
2830
  const best_for = best_for_raw.split(",").map((s) => s.trim()).filter(Boolean);
2752
2831
  const install = {};
2753
2832
  if (type === "mcp_server") {
2754
- const command = await input2({ message: "MCP command" });
2755
- const args_raw = await input2({ message: "MCP args, comma-separated (leave blank for none)" });
2833
+ const command = await input3({ message: "MCP command" });
2834
+ const args_raw = await input3({ message: "MCP args, comma-separated (leave blank for none)" });
2756
2835
  const args = args_raw.split(",").map((s) => s.trim()).filter(Boolean);
2757
2836
  install.mcp_config = { command, args };
2758
2837
  }