kairn-cli 1.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/kairn.js +2 -0
- package/dist/cli.js +794 -0
- package/dist/cli.js.map +1 -0
- package/package.json +55 -0
- package/src/registry/tools.json +340 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { Command as Command6 } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/commands/init.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { password, select } from "@inquirer/prompts";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
9
|
+
import OpenAI from "openai";
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import os from "os";
|
|
16
|
+
var KAIRN_DIR = path.join(os.homedir(), ".kairn");
|
|
17
|
+
var CONFIG_PATH = path.join(KAIRN_DIR, "config.json");
|
|
18
|
+
var ENVS_DIR = path.join(KAIRN_DIR, "envs");
|
|
19
|
+
function getConfigPath() {
|
|
20
|
+
return CONFIG_PATH;
|
|
21
|
+
}
|
|
22
|
+
function getEnvsDir() {
|
|
23
|
+
return ENVS_DIR;
|
|
24
|
+
}
|
|
25
|
+
async function ensureDirs() {
|
|
26
|
+
await fs.mkdir(KAIRN_DIR, { recursive: true });
|
|
27
|
+
await fs.mkdir(ENVS_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
async function loadConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const data = await fs.readFile(CONFIG_PATH, "utf-8");
|
|
32
|
+
return JSON.parse(data);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function saveConfig(config) {
|
|
38
|
+
await ensureDirs();
|
|
39
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/commands/init.ts
|
|
43
|
+
var PROVIDER_MODELS = {
|
|
44
|
+
anthropic: {
|
|
45
|
+
name: "Anthropic",
|
|
46
|
+
models: [
|
|
47
|
+
{ name: "Claude Sonnet 4 (recommended \u2014 fast, cheap)", value: "claude-sonnet-4-20250514" },
|
|
48
|
+
{ name: "Claude Opus 4 (highest quality)", value: "claude-opus-4-20250514" },
|
|
49
|
+
{ name: "Claude Haiku 3.5 (fastest, cheapest)", value: "claude-3-5-haiku-20241022" }
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
openai: {
|
|
53
|
+
name: "OpenAI",
|
|
54
|
+
models: [
|
|
55
|
+
{ name: "GPT-4o (recommended)", value: "gpt-4o" },
|
|
56
|
+
{ name: "GPT-4o mini (faster, cheaper)", value: "gpt-4o-mini" },
|
|
57
|
+
{ name: "o3 (reasoning)", value: "o3" }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
google: {
|
|
61
|
+
name: "Google Gemini",
|
|
62
|
+
models: [
|
|
63
|
+
{ name: "Gemini 2.5 Flash (recommended)", value: "gemini-2.5-flash-preview-05-20" },
|
|
64
|
+
{ name: "Gemini 2.5 Pro (highest quality)", value: "gemini-2.5-pro-preview-05-06" }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
async function verifyKey(provider, apiKey, model) {
|
|
69
|
+
try {
|
|
70
|
+
if (provider === "anthropic") {
|
|
71
|
+
const client = new Anthropic({ apiKey });
|
|
72
|
+
await client.messages.create({
|
|
73
|
+
model: "claude-3-5-haiku-20241022",
|
|
74
|
+
max_tokens: 10,
|
|
75
|
+
messages: [{ role: "user", content: "ping" }]
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
} else if (provider === "openai") {
|
|
79
|
+
const client = new OpenAI({ apiKey });
|
|
80
|
+
await client.chat.completions.create({
|
|
81
|
+
model: "gpt-4o-mini",
|
|
82
|
+
max_tokens: 10,
|
|
83
|
+
messages: [{ role: "user", content: "ping" }]
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
} else if (provider === "google") {
|
|
87
|
+
const client = new OpenAI({
|
|
88
|
+
apiKey,
|
|
89
|
+
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
90
|
+
});
|
|
91
|
+
await client.chat.completions.create({
|
|
92
|
+
model: "gemini-2.5-flash-preview-05-20",
|
|
93
|
+
max_tokens: 10,
|
|
94
|
+
messages: [{ role: "user", content: "ping" }]
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function detectClaudeCode() {
|
|
104
|
+
try {
|
|
105
|
+
execFileSync("which", ["claude"], { stdio: "ignore" });
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
var initCommand = new Command("init").description("Set up Kairn with your API key").action(async () => {
|
|
112
|
+
console.log(chalk.cyan("\n Kairn Setup\n"));
|
|
113
|
+
const existing = await loadConfig();
|
|
114
|
+
if (existing) {
|
|
115
|
+
console.log(
|
|
116
|
+
chalk.yellow(" Config already exists at ") + chalk.dim(getConfigPath())
|
|
117
|
+
);
|
|
118
|
+
console.log(chalk.yellow(" Running setup will overwrite it.\n"));
|
|
119
|
+
}
|
|
120
|
+
const provider = await select({
|
|
121
|
+
message: "LLM provider",
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: "Anthropic (Claude) \u2014 recommended", value: "anthropic" },
|
|
124
|
+
{ name: "OpenAI (GPT)", value: "openai" },
|
|
125
|
+
{ name: "Google (Gemini)", value: "google" }
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
const providerInfo = PROVIDER_MODELS[provider];
|
|
129
|
+
const model = await select({
|
|
130
|
+
message: "Compilation model",
|
|
131
|
+
choices: providerInfo.models
|
|
132
|
+
});
|
|
133
|
+
const apiKey = await password({
|
|
134
|
+
message: `${providerInfo.name} API key`,
|
|
135
|
+
mask: "*"
|
|
136
|
+
});
|
|
137
|
+
if (!apiKey) {
|
|
138
|
+
console.log(chalk.red("\n No API key provided. Aborting."));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
console.log(chalk.dim("\n Verifying API key..."));
|
|
142
|
+
const valid = await verifyKey(provider, apiKey, model);
|
|
143
|
+
if (!valid) {
|
|
144
|
+
console.log(
|
|
145
|
+
chalk.red(" Invalid API key. Check your key and try again.")
|
|
146
|
+
);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
console.log(chalk.green(" \u2713 API key verified"));
|
|
150
|
+
const config = {
|
|
151
|
+
provider,
|
|
152
|
+
api_key: apiKey,
|
|
153
|
+
model,
|
|
154
|
+
default_runtime: "claude-code",
|
|
155
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
156
|
+
};
|
|
157
|
+
await saveConfig(config);
|
|
158
|
+
console.log(
|
|
159
|
+
chalk.green(" \u2713 Config saved to ") + chalk.dim(getConfigPath())
|
|
160
|
+
);
|
|
161
|
+
console.log(
|
|
162
|
+
chalk.dim(` \u2713 Provider: ${providerInfo.name}, Model: ${model}`)
|
|
163
|
+
);
|
|
164
|
+
const hasClaude = detectClaudeCode();
|
|
165
|
+
if (hasClaude) {
|
|
166
|
+
console.log(chalk.green(" \u2713 Claude Code detected"));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(
|
|
169
|
+
chalk.yellow(
|
|
170
|
+
" \u26A0 Claude Code not found. Install it: npm install -g @anthropic-ai/claude-code"
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
console.log(
|
|
175
|
+
chalk.cyan("\n Ready! Run ") + chalk.bold("kairn describe") + chalk.cyan(" to create your first environment.\n")
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// src/commands/describe.ts
|
|
180
|
+
import { Command as Command2 } from "commander";
|
|
181
|
+
import { input, confirm } from "@inquirer/prompts";
|
|
182
|
+
import chalk2 from "chalk";
|
|
183
|
+
import fs4 from "fs/promises";
|
|
184
|
+
import path4 from "path";
|
|
185
|
+
|
|
186
|
+
// src/compiler/compile.ts
|
|
187
|
+
import fs2 from "fs/promises";
|
|
188
|
+
import path2 from "path";
|
|
189
|
+
import { fileURLToPath } from "url";
|
|
190
|
+
import crypto from "crypto";
|
|
191
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
192
|
+
import OpenAI2 from "openai";
|
|
193
|
+
|
|
194
|
+
// src/compiler/prompt.ts
|
|
195
|
+
var SYSTEM_PROMPT = `You are the Kairn environment compiler. Your job is to generate a minimal, optimal Claude Code agent environment from a user's natural language description of what they want their agent to do.
|
|
196
|
+
|
|
197
|
+
You will receive:
|
|
198
|
+
1. The user's intent (what they want to build/do)
|
|
199
|
+
2. A tool registry (available MCP servers, plugins, and hooks)
|
|
200
|
+
|
|
201
|
+
You must output a JSON object matching the EnvironmentSpec schema.
|
|
202
|
+
|
|
203
|
+
## Core Principles
|
|
204
|
+
|
|
205
|
+
- **Minimalism over completeness.** Fewer, well-chosen tools beat many generic ones. Each MCP server costs 500-2000 context tokens.
|
|
206
|
+
- **Workflow-specific, not generic.** Every instruction, command, and rule must relate to the user's actual workflow.
|
|
207
|
+
- **Concise CLAUDE.md.** Under 100 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
|
|
208
|
+
- **Security by default.** Always include deny rules for destructive commands and secret file access.
|
|
209
|
+
|
|
210
|
+
## What You Must Always Include
|
|
211
|
+
|
|
212
|
+
1. A concise, workflow-specific \`claude_md\` (the CLAUDE.md content)
|
|
213
|
+
2. A \`/project:help\` command that explains the environment
|
|
214
|
+
3. A \`/project:tasks\` command for task management via TODO.md
|
|
215
|
+
4. A \`docs/TODO.md\` file for continuity
|
|
216
|
+
5. A \`docs/DECISIONS.md\` file for architectural decisions
|
|
217
|
+
6. A \`docs/LEARNINGS.md\` file for non-obvious discoveries
|
|
218
|
+
7. A \`rules/continuity.md\` rule encouraging updates to DECISIONS.md and LEARNINGS.md
|
|
219
|
+
8. A \`rules/security.md\` rule with essential security instructions
|
|
220
|
+
9. settings.json with deny rules for \`rm -rf\`, \`curl|sh\`, reading \`.env\` and \`secrets/\`
|
|
221
|
+
|
|
222
|
+
## Tool Selection Rules
|
|
223
|
+
|
|
224
|
+
- Only select tools directly relevant to the described workflow
|
|
225
|
+
- Prefer free tools (auth: "none") when quality is comparable
|
|
226
|
+
- Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
|
|
227
|
+
- For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
|
|
228
|
+
- Maximum 6-8 MCP servers to avoid context bloat
|
|
229
|
+
- Include a \`reason\` for each selected tool explaining why it fits this workflow
|
|
230
|
+
|
|
231
|
+
## For Code Projects, Additionally Include
|
|
232
|
+
|
|
233
|
+
- \`/project:plan\` command (plan before coding)
|
|
234
|
+
- \`/project:review\` command (review changes)
|
|
235
|
+
- \`/project:test\` command (run and fix tests)
|
|
236
|
+
- \`/project:commit\` command (conventional commits)
|
|
237
|
+
- A TDD skill if testing is relevant
|
|
238
|
+
- A reviewer agent (read-only, Sonnet model)
|
|
239
|
+
|
|
240
|
+
## For Research Projects, Additionally Include
|
|
241
|
+
|
|
242
|
+
- \`/project:research\` command (deep research on a topic)
|
|
243
|
+
- \`/project:summarize\` command (summarize findings)
|
|
244
|
+
- A research-synthesis skill
|
|
245
|
+
- A researcher agent
|
|
246
|
+
|
|
247
|
+
## For Content/Writing Projects, Additionally Include
|
|
248
|
+
|
|
249
|
+
- \`/project:draft\` command (write first draft)
|
|
250
|
+
- \`/project:edit\` command (review and improve writing)
|
|
251
|
+
- A writing-workflow skill
|
|
252
|
+
|
|
253
|
+
## Output Schema
|
|
254
|
+
|
|
255
|
+
Return ONLY valid JSON matching this structure:
|
|
256
|
+
|
|
257
|
+
\`\`\`json
|
|
258
|
+
{
|
|
259
|
+
"name": "short-kebab-case-name",
|
|
260
|
+
"description": "One-line description of the environment",
|
|
261
|
+
"tools": [
|
|
262
|
+
{ "tool_id": "id-from-registry", "reason": "why this tool fits" }
|
|
263
|
+
],
|
|
264
|
+
"harness": {
|
|
265
|
+
"claude_md": "The full CLAUDE.md content (under 100 lines)",
|
|
266
|
+
"settings": {
|
|
267
|
+
"permissions": {
|
|
268
|
+
"allow": ["Bash(npm run *)", "Read", "Write", "Edit"],
|
|
269
|
+
"deny": ["Bash(rm -rf *)", "Bash(curl * | sh)", "Read(./.env)", "Read(./secrets/**)"]
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"mcp_config": {
|
|
273
|
+
"server-name": { "command": "npx", "args": ["..."], "env": {} }
|
|
274
|
+
},
|
|
275
|
+
"commands": {
|
|
276
|
+
"help": "markdown content for /project:help",
|
|
277
|
+
"tasks": "markdown content for /project:tasks"
|
|
278
|
+
},
|
|
279
|
+
"rules": {
|
|
280
|
+
"continuity": "markdown content for continuity rule",
|
|
281
|
+
"security": "markdown content for security rule"
|
|
282
|
+
},
|
|
283
|
+
"skills": {
|
|
284
|
+
"skill-name/SKILL": "markdown content with YAML frontmatter"
|
|
285
|
+
},
|
|
286
|
+
"agents": {
|
|
287
|
+
"agent-name": "markdown content with YAML frontmatter"
|
|
288
|
+
},
|
|
289
|
+
"docs": {
|
|
290
|
+
"TODO": "# TODO\\n\\n- [ ] First task based on workflow",
|
|
291
|
+
"DECISIONS": "# Decisions\\n\\nArchitectural decisions for this project.",
|
|
292
|
+
"LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries and gotchas."
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
\`\`\`
|
|
297
|
+
|
|
298
|
+
Do not include any text outside the JSON object. Do not wrap in markdown code fences.`;
|
|
299
|
+
|
|
300
|
+
// src/compiler/compile.ts
|
|
301
|
+
async function loadRegistry() {
|
|
302
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
303
|
+
const __dirname = path2.dirname(__filename);
|
|
304
|
+
const candidates = [
|
|
305
|
+
path2.resolve(__dirname, "../registry/tools.json"),
|
|
306
|
+
path2.resolve(__dirname, "../src/registry/tools.json"),
|
|
307
|
+
path2.resolve(__dirname, "../../src/registry/tools.json")
|
|
308
|
+
];
|
|
309
|
+
for (const candidate of candidates) {
|
|
310
|
+
try {
|
|
311
|
+
const data = await fs2.readFile(candidate, "utf-8");
|
|
312
|
+
return JSON.parse(data);
|
|
313
|
+
} catch {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
throw new Error("Could not find tools.json registry");
|
|
318
|
+
}
|
|
319
|
+
function buildUserMessage(intent, registry) {
|
|
320
|
+
const registrySummary = registry.map(
|
|
321
|
+
(t) => `- ${t.id} (${t.type}, tier ${t.tier}, auth: ${t.auth}): ${t.description} [best_for: ${t.best_for.join(", ")}]`
|
|
322
|
+
).join("\n");
|
|
323
|
+
return `## User Intent
|
|
324
|
+
|
|
325
|
+
${intent}
|
|
326
|
+
|
|
327
|
+
## Available Tool Registry
|
|
328
|
+
|
|
329
|
+
${registrySummary}
|
|
330
|
+
|
|
331
|
+
Generate the EnvironmentSpec JSON now.`;
|
|
332
|
+
}
|
|
333
|
+
function parseSpecResponse(text) {
|
|
334
|
+
let cleaned = text.trim();
|
|
335
|
+
if (cleaned.startsWith("```")) {
|
|
336
|
+
cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
337
|
+
}
|
|
338
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
339
|
+
if (!jsonMatch) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
"LLM response did not contain valid JSON. Try again or use a different model."
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
return JSON.parse(jsonMatch[0]);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Failed to parse LLM response as JSON: ${err instanceof Error ? err.message : String(err)}
|
|
349
|
+
Response started with: ${cleaned.slice(0, 200)}...`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function classifyError(err, provider) {
|
|
354
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
355
|
+
const status = err?.status;
|
|
356
|
+
const code = err?.code;
|
|
357
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT") {
|
|
358
|
+
return `Network error: could not reach ${provider} API. Check your internet connection.`;
|
|
359
|
+
}
|
|
360
|
+
if (status === 401 || msg.includes("invalid") && msg.includes("key")) {
|
|
361
|
+
return `Invalid API key for ${provider}. Run \`kairn init\` to reconfigure.`;
|
|
362
|
+
}
|
|
363
|
+
if (status === 403) {
|
|
364
|
+
return `Access denied by ${provider}. Your API key may lack permissions for this model.`;
|
|
365
|
+
}
|
|
366
|
+
if (status === 429 || msg.includes("rate limit") || msg.includes("quota")) {
|
|
367
|
+
return `Rate limited by ${provider}. Wait a moment and try again, or switch to a cheaper model with \`kairn init\`.`;
|
|
368
|
+
}
|
|
369
|
+
if (status === 404 || msg.includes("not found") || msg.includes("does not exist")) {
|
|
370
|
+
return `Model not found on ${provider}. Run \`kairn init\` to select a valid model.`;
|
|
371
|
+
}
|
|
372
|
+
if (status === 529 || status === 503 || msg.includes("overloaded")) {
|
|
373
|
+
return `${provider} is temporarily overloaded. Try again in a few seconds.`;
|
|
374
|
+
}
|
|
375
|
+
if (msg.includes("token") && (msg.includes("limit") || msg.includes("exceed"))) {
|
|
376
|
+
return `Request too large for the selected model. Try a shorter workflow description.`;
|
|
377
|
+
}
|
|
378
|
+
if (msg.includes("billing") || msg.includes("payment") || msg.includes("insufficient")) {
|
|
379
|
+
return `Billing issue with your ${provider} account. Check your account dashboard.`;
|
|
380
|
+
}
|
|
381
|
+
return `${provider} API error: ${msg}`;
|
|
382
|
+
}
|
|
383
|
+
async function callLLM(config, userMessage) {
|
|
384
|
+
if (config.provider === "anthropic") {
|
|
385
|
+
const client = new Anthropic2({ apiKey: config.api_key });
|
|
386
|
+
try {
|
|
387
|
+
const response = await client.messages.create({
|
|
388
|
+
model: config.model,
|
|
389
|
+
max_tokens: 8192,
|
|
390
|
+
system: SYSTEM_PROMPT,
|
|
391
|
+
messages: [{ role: "user", content: userMessage }]
|
|
392
|
+
});
|
|
393
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
394
|
+
if (!textBlock || textBlock.type !== "text") {
|
|
395
|
+
throw new Error("No text response from compiler LLM");
|
|
396
|
+
}
|
|
397
|
+
return textBlock.text;
|
|
398
|
+
} catch (err) {
|
|
399
|
+
throw new Error(classifyError(err, "Anthropic"));
|
|
400
|
+
}
|
|
401
|
+
} else if (config.provider === "openai" || config.provider === "google") {
|
|
402
|
+
const providerName = config.provider === "google" ? "Google" : "OpenAI";
|
|
403
|
+
const clientOptions = { apiKey: config.api_key };
|
|
404
|
+
if (config.provider === "google") {
|
|
405
|
+
clientOptions.baseURL = "https://generativelanguage.googleapis.com/v1beta/openai/";
|
|
406
|
+
}
|
|
407
|
+
const client = new OpenAI2(clientOptions);
|
|
408
|
+
try {
|
|
409
|
+
const response = await client.chat.completions.create({
|
|
410
|
+
model: config.model,
|
|
411
|
+
max_tokens: 8192,
|
|
412
|
+
messages: [
|
|
413
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
414
|
+
{ role: "user", content: userMessage }
|
|
415
|
+
]
|
|
416
|
+
});
|
|
417
|
+
const text = response.choices[0]?.message?.content;
|
|
418
|
+
if (!text) {
|
|
419
|
+
throw new Error("No text response from compiler LLM");
|
|
420
|
+
}
|
|
421
|
+
return text;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
throw new Error(classifyError(err, providerName));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`Unsupported provider: ${config.provider}. Run \`kairn init\` to reconfigure.`);
|
|
427
|
+
}
|
|
428
|
+
async function compile(intent, onProgress) {
|
|
429
|
+
const config = await loadConfig();
|
|
430
|
+
if (!config) {
|
|
431
|
+
throw new Error("No config found. Run `kairn init` first.");
|
|
432
|
+
}
|
|
433
|
+
onProgress?.("Loading tool registry...");
|
|
434
|
+
const registry = await loadRegistry();
|
|
435
|
+
onProgress?.(`Compiling with ${config.provider} (${config.model})...`);
|
|
436
|
+
const userMessage = buildUserMessage(intent, registry);
|
|
437
|
+
const responseText = await callLLM(config, userMessage);
|
|
438
|
+
onProgress?.("Parsing environment spec...");
|
|
439
|
+
const parsed = parseSpecResponse(responseText);
|
|
440
|
+
const spec = {
|
|
441
|
+
id: `env_${crypto.randomUUID()}`,
|
|
442
|
+
intent,
|
|
443
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
444
|
+
...parsed
|
|
445
|
+
};
|
|
446
|
+
await ensureDirs();
|
|
447
|
+
const envPath = path2.join(getEnvsDir(), `${spec.id}.json`);
|
|
448
|
+
await fs2.writeFile(envPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
449
|
+
return spec;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/adapter/claude-code.ts
|
|
453
|
+
import fs3 from "fs/promises";
|
|
454
|
+
import path3 from "path";
|
|
455
|
+
async function writeFile(filePath, content) {
|
|
456
|
+
await fs3.mkdir(path3.dirname(filePath), { recursive: true });
|
|
457
|
+
await fs3.writeFile(filePath, content, "utf-8");
|
|
458
|
+
}
|
|
459
|
+
async function writeEnvironment(spec, targetDir) {
|
|
460
|
+
const claudeDir = path3.join(targetDir, ".claude");
|
|
461
|
+
const written = [];
|
|
462
|
+
if (spec.harness.claude_md) {
|
|
463
|
+
const p = path3.join(claudeDir, "CLAUDE.md");
|
|
464
|
+
await writeFile(p, spec.harness.claude_md);
|
|
465
|
+
written.push(".claude/CLAUDE.md");
|
|
466
|
+
}
|
|
467
|
+
if (spec.harness.settings && Object.keys(spec.harness.settings).length > 0) {
|
|
468
|
+
const p = path3.join(claudeDir, "settings.json");
|
|
469
|
+
await writeFile(p, JSON.stringify(spec.harness.settings, null, 2));
|
|
470
|
+
written.push(".claude/settings.json");
|
|
471
|
+
}
|
|
472
|
+
if (spec.harness.mcp_config && Object.keys(spec.harness.mcp_config).length > 0) {
|
|
473
|
+
const p = path3.join(targetDir, ".mcp.json");
|
|
474
|
+
const mcpContent = { mcpServers: spec.harness.mcp_config };
|
|
475
|
+
await writeFile(p, JSON.stringify(mcpContent, null, 2));
|
|
476
|
+
written.push(".mcp.json");
|
|
477
|
+
}
|
|
478
|
+
if (spec.harness.commands) {
|
|
479
|
+
for (const [name, content] of Object.entries(spec.harness.commands)) {
|
|
480
|
+
const p = path3.join(claudeDir, "commands", `${name}.md`);
|
|
481
|
+
await writeFile(p, content);
|
|
482
|
+
written.push(`.claude/commands/${name}.md`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (spec.harness.rules) {
|
|
486
|
+
for (const [name, content] of Object.entries(spec.harness.rules)) {
|
|
487
|
+
const p = path3.join(claudeDir, "rules", `${name}.md`);
|
|
488
|
+
await writeFile(p, content);
|
|
489
|
+
written.push(`.claude/rules/${name}.md`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (spec.harness.skills) {
|
|
493
|
+
for (const [skillPath, content] of Object.entries(spec.harness.skills)) {
|
|
494
|
+
const p = path3.join(claudeDir, "skills", `${skillPath}.md`);
|
|
495
|
+
await writeFile(p, content);
|
|
496
|
+
written.push(`.claude/skills/${skillPath}.md`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (spec.harness.agents) {
|
|
500
|
+
for (const [name, content] of Object.entries(spec.harness.agents)) {
|
|
501
|
+
const p = path3.join(claudeDir, "agents", `${name}.md`);
|
|
502
|
+
await writeFile(p, content);
|
|
503
|
+
written.push(`.claude/agents/${name}.md`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (spec.harness.docs) {
|
|
507
|
+
for (const [name, content] of Object.entries(spec.harness.docs)) {
|
|
508
|
+
const p = path3.join(claudeDir, "docs", `${name}.md`);
|
|
509
|
+
await writeFile(p, content);
|
|
510
|
+
written.push(`.claude/docs/${name}.md`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return written;
|
|
514
|
+
}
|
|
515
|
+
function summarizeSpec(spec, registry) {
|
|
516
|
+
const pluginCommands = [];
|
|
517
|
+
for (const selected of spec.tools) {
|
|
518
|
+
const tool = registry.find((t) => t.id === selected.tool_id);
|
|
519
|
+
if (tool?.install.plugin_command) {
|
|
520
|
+
pluginCommands.push(tool.install.plugin_command);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
toolCount: spec.tools.length,
|
|
525
|
+
commandCount: Object.keys(spec.harness.commands || {}).length,
|
|
526
|
+
ruleCount: Object.keys(spec.harness.rules || {}).length,
|
|
527
|
+
skillCount: Object.keys(spec.harness.skills || {}).length,
|
|
528
|
+
agentCount: Object.keys(spec.harness.agents || {}).length,
|
|
529
|
+
pluginCommands
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/commands/describe.ts
|
|
534
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
535
|
+
async function loadRegistry2() {
|
|
536
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
537
|
+
const __dirname = path4.dirname(__filename);
|
|
538
|
+
const candidates = [
|
|
539
|
+
path4.resolve(__dirname, "../registry/tools.json"),
|
|
540
|
+
path4.resolve(__dirname, "../src/registry/tools.json"),
|
|
541
|
+
path4.resolve(__dirname, "../../src/registry/tools.json")
|
|
542
|
+
];
|
|
543
|
+
for (const candidate of candidates) {
|
|
544
|
+
try {
|
|
545
|
+
const data = await fs4.readFile(candidate, "utf-8");
|
|
546
|
+
return JSON.parse(data);
|
|
547
|
+
} catch {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw new Error("Could not find tools.json registry");
|
|
552
|
+
}
|
|
553
|
+
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").action(async (intentArg, options) => {
|
|
554
|
+
const config = await loadConfig();
|
|
555
|
+
if (!config) {
|
|
556
|
+
console.log(
|
|
557
|
+
chalk2.red("\n No config found. Run ") + chalk2.bold("kairn init") + chalk2.red(" first.\n")
|
|
558
|
+
);
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
const intent = intentArg || await input({
|
|
562
|
+
message: "What do you want your agent to do?"
|
|
563
|
+
});
|
|
564
|
+
if (!intent.trim()) {
|
|
565
|
+
console.log(chalk2.red("\n No description provided. Aborting.\n"));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
console.log("");
|
|
569
|
+
let spec;
|
|
570
|
+
try {
|
|
571
|
+
spec = await compile(intent, (msg) => {
|
|
572
|
+
process.stdout.write(`\r ${chalk2.dim(msg)} `);
|
|
573
|
+
});
|
|
574
|
+
process.stdout.write("\r \r");
|
|
575
|
+
} catch (err) {
|
|
576
|
+
process.stdout.write("\r \r");
|
|
577
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
578
|
+
console.log(chalk2.red(`
|
|
579
|
+
Compilation failed: ${msg}
|
|
580
|
+
`));
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
const registry = await loadRegistry2();
|
|
584
|
+
const summary = summarizeSpec(spec, registry);
|
|
585
|
+
console.log(chalk2.green("\n \u2713 Environment compiled\n"));
|
|
586
|
+
console.log(chalk2.cyan(" Name: ") + spec.name);
|
|
587
|
+
console.log(chalk2.cyan(" Description: ") + spec.description);
|
|
588
|
+
console.log(chalk2.cyan(" Tools: ") + summary.toolCount);
|
|
589
|
+
console.log(chalk2.cyan(" Commands: ") + summary.commandCount);
|
|
590
|
+
console.log(chalk2.cyan(" Rules: ") + summary.ruleCount);
|
|
591
|
+
console.log(chalk2.cyan(" Skills: ") + summary.skillCount);
|
|
592
|
+
console.log(chalk2.cyan(" Agents: ") + summary.agentCount);
|
|
593
|
+
if (spec.tools.length > 0) {
|
|
594
|
+
console.log(chalk2.dim("\n Selected tools:"));
|
|
595
|
+
for (const tool of spec.tools) {
|
|
596
|
+
const regTool = registry.find((t) => t.id === tool.tool_id);
|
|
597
|
+
const name = regTool?.name || tool.tool_id;
|
|
598
|
+
console.log(chalk2.dim(` - ${name}: ${tool.reason}`));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (summary.pluginCommands.length > 0) {
|
|
602
|
+
console.log(chalk2.yellow("\n Plugins to install manually:"));
|
|
603
|
+
for (const cmd of summary.pluginCommands) {
|
|
604
|
+
console.log(chalk2.yellow(` ${cmd}`));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
console.log("");
|
|
608
|
+
const proceed = options.yes || await confirm({
|
|
609
|
+
message: "Generate environment in current directory?",
|
|
610
|
+
default: true
|
|
611
|
+
});
|
|
612
|
+
if (!proceed) {
|
|
613
|
+
console.log(chalk2.dim("\n Aborted. Environment saved to ~/.kairn/envs/\n"));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const targetDir = process.cwd();
|
|
617
|
+
const written = await writeEnvironment(spec, targetDir);
|
|
618
|
+
console.log(chalk2.green("\n \u2713 Environment written\n"));
|
|
619
|
+
for (const file of written) {
|
|
620
|
+
console.log(chalk2.dim(` ${file}`));
|
|
621
|
+
}
|
|
622
|
+
if (summary.pluginCommands.length > 0) {
|
|
623
|
+
console.log(chalk2.yellow("\n Install plugins by running these in Claude Code:"));
|
|
624
|
+
for (const cmd of summary.pluginCommands) {
|
|
625
|
+
console.log(chalk2.bold(` ${cmd}`));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
console.log(
|
|
629
|
+
chalk2.cyan("\n Ready! Run ") + chalk2.bold("claude") + chalk2.cyan(" to start.\n")
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// src/commands/list.ts
|
|
634
|
+
import { Command as Command3 } from "commander";
|
|
635
|
+
import chalk3 from "chalk";
|
|
636
|
+
import fs5 from "fs/promises";
|
|
637
|
+
import path5 from "path";
|
|
638
|
+
var listCommand = new Command3("list").description("Show saved environments").action(async () => {
|
|
639
|
+
const envsDir = getEnvsDir();
|
|
640
|
+
let files;
|
|
641
|
+
try {
|
|
642
|
+
files = await fs5.readdir(envsDir);
|
|
643
|
+
} catch {
|
|
644
|
+
console.log(chalk3.dim("\n No environments yet. Run ") + chalk3.bold("kairn describe") + chalk3.dim(" to create one.\n"));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
648
|
+
if (jsonFiles.length === 0) {
|
|
649
|
+
console.log(chalk3.dim("\n No environments yet. Run ") + chalk3.bold("kairn describe") + chalk3.dim(" to create one.\n"));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
console.log(chalk3.cyan("\n Saved Environments\n"));
|
|
653
|
+
for (const file of jsonFiles) {
|
|
654
|
+
try {
|
|
655
|
+
const data = await fs5.readFile(path5.join(envsDir, file), "utf-8");
|
|
656
|
+
const spec = JSON.parse(data);
|
|
657
|
+
const date = new Date(spec.created_at).toLocaleDateString();
|
|
658
|
+
const toolCount = spec.tools?.length ?? 0;
|
|
659
|
+
console.log(chalk3.bold(` ${spec.name}`));
|
|
660
|
+
console.log(chalk3.dim(` ${spec.description}`));
|
|
661
|
+
console.log(
|
|
662
|
+
chalk3.dim(` ${date} \xB7 ${toolCount} tools \xB7 ${spec.id}`)
|
|
663
|
+
);
|
|
664
|
+
console.log("");
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// src/commands/activate.ts
|
|
671
|
+
import { Command as Command4 } from "commander";
|
|
672
|
+
import chalk4 from "chalk";
|
|
673
|
+
import fs6 from "fs/promises";
|
|
674
|
+
import path6 from "path";
|
|
675
|
+
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) => {
|
|
676
|
+
const envsDir = getEnvsDir();
|
|
677
|
+
let files;
|
|
678
|
+
try {
|
|
679
|
+
files = await fs6.readdir(envsDir);
|
|
680
|
+
} catch {
|
|
681
|
+
console.log(chalk4.red("\n No saved environments found.\n"));
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
const match = files.find(
|
|
685
|
+
(f) => f === `${envId}.json` || f.startsWith(envId)
|
|
686
|
+
);
|
|
687
|
+
if (!match) {
|
|
688
|
+
console.log(chalk4.red(`
|
|
689
|
+
Environment "${envId}" not found.`));
|
|
690
|
+
console.log(chalk4.dim(" Run kairn list to see saved environments.\n"));
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
const data = await fs6.readFile(path6.join(envsDir, match), "utf-8");
|
|
694
|
+
const spec = JSON.parse(data);
|
|
695
|
+
console.log(chalk4.cyan(`
|
|
696
|
+
Activating: ${spec.name}`));
|
|
697
|
+
console.log(chalk4.dim(` ${spec.description}
|
|
698
|
+
`));
|
|
699
|
+
const targetDir = process.cwd();
|
|
700
|
+
const written = await writeEnvironment(spec, targetDir);
|
|
701
|
+
console.log(chalk4.green(" \u2713 Environment written\n"));
|
|
702
|
+
for (const file of written) {
|
|
703
|
+
console.log(chalk4.dim(` ${file}`));
|
|
704
|
+
}
|
|
705
|
+
console.log(
|
|
706
|
+
chalk4.cyan("\n Ready! Run ") + chalk4.bold("claude") + chalk4.cyan(" to start.\n")
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// src/commands/update-registry.ts
|
|
711
|
+
import { Command as Command5 } from "commander";
|
|
712
|
+
import chalk5 from "chalk";
|
|
713
|
+
import fs7 from "fs/promises";
|
|
714
|
+
import path7 from "path";
|
|
715
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
716
|
+
var REGISTRY_URL = "https://raw.githubusercontent.com/ashtonperlroth/kairn/main/src/registry/tools.json";
|
|
717
|
+
async function getLocalRegistryPath() {
|
|
718
|
+
const __filename = fileURLToPath3(import.meta.url);
|
|
719
|
+
const __dirname = path7.dirname(__filename);
|
|
720
|
+
const candidates = [
|
|
721
|
+
path7.resolve(__dirname, "../registry/tools.json"),
|
|
722
|
+
path7.resolve(__dirname, "../src/registry/tools.json"),
|
|
723
|
+
path7.resolve(__dirname, "../../src/registry/tools.json")
|
|
724
|
+
];
|
|
725
|
+
for (const candidate of candidates) {
|
|
726
|
+
try {
|
|
727
|
+
await fs7.access(candidate);
|
|
728
|
+
return candidate;
|
|
729
|
+
} catch {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
throw new Error("Could not find local tools.json registry");
|
|
734
|
+
}
|
|
735
|
+
var updateRegistryCommand = new Command5("update-registry").description("Fetch the latest tool registry from GitHub").option("--url <url>", "Custom registry URL").action(async (options) => {
|
|
736
|
+
const url = options.url || REGISTRY_URL;
|
|
737
|
+
console.log(chalk5.dim(`
|
|
738
|
+
Fetching registry from ${url}...`));
|
|
739
|
+
try {
|
|
740
|
+
const response = await fetch(url);
|
|
741
|
+
if (!response.ok) {
|
|
742
|
+
console.log(
|
|
743
|
+
chalk5.red(` Failed to fetch registry: ${response.status} ${response.statusText}`)
|
|
744
|
+
);
|
|
745
|
+
console.log(
|
|
746
|
+
chalk5.dim(" The remote registry may not be available yet.")
|
|
747
|
+
);
|
|
748
|
+
console.log(
|
|
749
|
+
chalk5.dim(" Your local registry is still active.\n")
|
|
750
|
+
);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const text = await response.text();
|
|
754
|
+
let tools;
|
|
755
|
+
try {
|
|
756
|
+
tools = JSON.parse(text);
|
|
757
|
+
if (!Array.isArray(tools)) throw new Error("Not an array");
|
|
758
|
+
if (tools.length === 0) throw new Error("Empty registry");
|
|
759
|
+
} catch (err) {
|
|
760
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
761
|
+
console.log(chalk5.red(` Invalid registry format: ${msg}
|
|
762
|
+
`));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const registryPath = await getLocalRegistryPath();
|
|
766
|
+
const backupPath = registryPath + ".bak";
|
|
767
|
+
try {
|
|
768
|
+
await fs7.copyFile(registryPath, backupPath);
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
await fs7.writeFile(registryPath, JSON.stringify(tools, null, 2), "utf-8");
|
|
772
|
+
console.log(chalk5.green(` \u2713 Registry updated: ${tools.length} tools`));
|
|
773
|
+
console.log(chalk5.dim(` Saved to: ${registryPath}`));
|
|
774
|
+
console.log(chalk5.dim(` Backup: ${backupPath}
|
|
775
|
+
`));
|
|
776
|
+
} catch (err) {
|
|
777
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
778
|
+
console.log(chalk5.red(` Network error: ${msg}`));
|
|
779
|
+
console.log(chalk5.dim(" Your local registry is still active.\n"));
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// src/cli.ts
|
|
784
|
+
var program = new Command6();
|
|
785
|
+
program.name("kairn").description(
|
|
786
|
+
"Compile natural language intent into optimized Claude Code environments"
|
|
787
|
+
).version("0.1.0");
|
|
788
|
+
program.addCommand(initCommand);
|
|
789
|
+
program.addCommand(describeCommand);
|
|
790
|
+
program.addCommand(listCommand);
|
|
791
|
+
program.addCommand(activateCommand);
|
|
792
|
+
program.addCommand(updateRegistryCommand);
|
|
793
|
+
program.parse();
|
|
794
|
+
//# sourceMappingURL=cli.js.map
|