kairn-cli 1.9.1 → 1.10.1
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 +529 -167
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
266
|
-
await
|
|
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
|
-
|
|
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: `${
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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",
|
|
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
|
|
|
@@ -376,17 +460,72 @@ import Anthropic2 from "@anthropic-ai/sdk";
|
|
|
376
460
|
import OpenAI2 from "openai";
|
|
377
461
|
|
|
378
462
|
// src/compiler/prompt.ts
|
|
379
|
-
var
|
|
463
|
+
var SKELETON_PROMPT = `You are the Kairn skeleton compiler. Your job is to select tools and outline the project structure from a user's natural language description.
|
|
380
464
|
|
|
381
465
|
You will receive:
|
|
382
466
|
1. The user's intent (what they want to build/do)
|
|
383
467
|
2. A tool registry (available MCP servers, plugins, and hooks)
|
|
384
468
|
|
|
385
|
-
You must output a JSON object matching the
|
|
469
|
+
You must output a JSON object matching the SkeletonSpec schema.
|
|
386
470
|
|
|
387
471
|
## Core Principles
|
|
388
472
|
|
|
389
473
|
- **Minimalism over completeness.** Fewer, well-chosen tools beat many generic ones. Each MCP server costs 500-2000 context tokens.
|
|
474
|
+
- **Workflow-specific, not generic.** Select tools that directly support the user's actual workflow.
|
|
475
|
+
- **Security by default.** Essential for all projects.
|
|
476
|
+
|
|
477
|
+
## Tool Selection Rules
|
|
478
|
+
|
|
479
|
+
- Only select tools directly relevant to the described workflow
|
|
480
|
+
- Prefer free tools (auth: "none") when quality is comparable
|
|
481
|
+
- Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
|
|
482
|
+
- For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
|
|
483
|
+
- Maximum 6-8 MCP servers to avoid context bloat
|
|
484
|
+
- Include a \`reason\` for each selected tool explaining why it fits this workflow
|
|
485
|
+
|
|
486
|
+
## Context Budget (STRICT)
|
|
487
|
+
|
|
488
|
+
- MCP servers: maximum 6. Prefer fewer.
|
|
489
|
+
- Skills: maximum 3. Only include directly relevant ones.
|
|
490
|
+
- Agents: maximum 3. QA pipeline + one specialist.
|
|
491
|
+
- Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
|
|
492
|
+
|
|
493
|
+
If the workflow doesn't clearly need a tool, DO NOT include it.
|
|
494
|
+
Each MCP server costs 500-2000 tokens of context window.
|
|
495
|
+
|
|
496
|
+
## Output Schema
|
|
497
|
+
|
|
498
|
+
Return ONLY valid JSON matching this structure:
|
|
499
|
+
|
|
500
|
+
\`\`\`json
|
|
501
|
+
{
|
|
502
|
+
"name": "short-kebab-case-name",
|
|
503
|
+
"description": "One-line description",
|
|
504
|
+
"tools": [
|
|
505
|
+
{ "tool_id": "id-from-registry", "reason": "why this tool fits" }
|
|
506
|
+
],
|
|
507
|
+
"outline": {
|
|
508
|
+
"tech_stack": ["Python", "pandas"],
|
|
509
|
+
"workflow_type": "data-analysis",
|
|
510
|
+
"key_commands": ["ingest", "analyze", "report"],
|
|
511
|
+
"custom_rules": ["data-integrity"],
|
|
512
|
+
"custom_agents": ["data-reviewer"],
|
|
513
|
+
"custom_skills": ["ms-data-analysis"]
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
\`\`\`
|
|
517
|
+
|
|
518
|
+
Return ONLY valid JSON. No markdown fences. No text outside the JSON.`;
|
|
519
|
+
var HARNESS_PROMPT = `You are the Kairn harness compiler. Your job is to generate the full environment content from a project skeleton.
|
|
520
|
+
|
|
521
|
+
You will receive:
|
|
522
|
+
1. The skeleton (tool selections + project outline)
|
|
523
|
+
2. The user's original intent
|
|
524
|
+
|
|
525
|
+
You must generate all harness content: CLAUDE.md, commands, rules, agents, skills, and docs.
|
|
526
|
+
|
|
527
|
+
## Core Principles
|
|
528
|
+
|
|
390
529
|
- **Workflow-specific, not generic.** Every instruction, command, and rule must relate to the user's actual workflow.
|
|
391
530
|
- **Concise CLAUDE.md.** Under 120 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
|
|
392
531
|
- **Security by default.** Always include deny rules for destructive commands and secret file access.
|
|
@@ -572,28 +711,6 @@ All projects should include a PostCompact hook to restore context after compacti
|
|
|
572
711
|
|
|
573
712
|
Merge this into the settings hooks alongside the PreToolUse and PostToolUse hooks.
|
|
574
713
|
|
|
575
|
-
## Tool Selection Rules
|
|
576
|
-
|
|
577
|
-
- Only select tools directly relevant to the described workflow
|
|
578
|
-
- Prefer free tools (auth: "none") when quality is comparable
|
|
579
|
-
- Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
|
|
580
|
-
- For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
|
|
581
|
-
- Maximum 6-8 MCP servers to avoid context bloat
|
|
582
|
-
- Include a \`reason\` for each selected tool explaining why it fits this workflow
|
|
583
|
-
|
|
584
|
-
## Context Budget (STRICT)
|
|
585
|
-
|
|
586
|
-
- MCP servers: maximum 6. Prefer fewer.
|
|
587
|
-
- CLAUDE.md: maximum 120 lines.
|
|
588
|
-
- Rules: maximum 5 files, each under 20 lines.
|
|
589
|
-
- Skills: maximum 3. Only include directly relevant ones.
|
|
590
|
-
- Agents: maximum 3. QA pipeline + one specialist.
|
|
591
|
-
- Commands: no limit (loaded on demand, zero context cost).
|
|
592
|
-
- Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
|
|
593
|
-
|
|
594
|
-
If the workflow doesn't clearly need a tool, DO NOT include it.
|
|
595
|
-
Each MCP server costs 500-2000 tokens of context window.
|
|
596
|
-
|
|
597
714
|
## For Code Projects, Additionally Include
|
|
598
715
|
|
|
599
716
|
- \`/project:plan\` command (plan before coding)
|
|
@@ -657,6 +774,133 @@ If no autonomy level is specified, assume Level 1 (Guided).
|
|
|
657
774
|
|
|
658
775
|
Return ONLY valid JSON matching this structure:
|
|
659
776
|
|
|
777
|
+
\`\`\`json
|
|
778
|
+
{
|
|
779
|
+
"claude_md": "Full CLAUDE.md content (under 120 lines)",
|
|
780
|
+
"commands": { "help": "...", "tasks": "...", "status": "...", "fix": "...", "sprint": "...", "spec": "...", "prove": "...", "grill": "...", "reset": "..." },
|
|
781
|
+
"rules": { "continuity": "...", "security": "..." },
|
|
782
|
+
"agents": { "qa-orchestrator": "...", "linter": "...", "e2e-tester": "..." },
|
|
783
|
+
"skills": { "skill-name/SKILL": "..." },
|
|
784
|
+
"docs": { "TODO": "...", "DECISIONS": "...", "LEARNINGS": "...", "SPRINT": "..." }
|
|
785
|
+
}
|
|
786
|
+
\`\`\`
|
|
787
|
+
|
|
788
|
+
Return ONLY valid JSON. No markdown fences. No text outside the JSON.`;
|
|
789
|
+
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.
|
|
790
|
+
|
|
791
|
+
You will receive:
|
|
792
|
+
1. The user's intent (what they want to build/do)
|
|
793
|
+
2. A tool registry (available MCP servers, plugins, and hooks)
|
|
794
|
+
|
|
795
|
+
You must output a JSON object matching the EnvironmentSpec schema.
|
|
796
|
+
|
|
797
|
+
## Core Principles
|
|
798
|
+
|
|
799
|
+
- **Minimalism over completeness.** Fewer, well-chosen tools beat many generic ones. Each MCP server costs 500-2000 context tokens.
|
|
800
|
+
- **Workflow-specific, not generic.** Every instruction, command, and rule must relate to the user's actual workflow.
|
|
801
|
+
- **Concise CLAUDE.md.** Under 120 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
|
|
802
|
+
- **Security by default.** Always include deny rules for destructive commands and secret file access.
|
|
803
|
+
|
|
804
|
+
## CLAUDE.md Template (mandatory structure)
|
|
805
|
+
|
|
806
|
+
The \`claude_md\` field MUST follow this exact structure (max 120 lines):
|
|
807
|
+
|
|
808
|
+
\`\`\`
|
|
809
|
+
# {Project Name}
|
|
810
|
+
|
|
811
|
+
## Purpose
|
|
812
|
+
{one-line description}
|
|
813
|
+
|
|
814
|
+
## Tech Stack
|
|
815
|
+
{bullet list of frameworks/languages}
|
|
816
|
+
|
|
817
|
+
## Commands
|
|
818
|
+
{concrete build/test/lint/dev commands}
|
|
819
|
+
|
|
820
|
+
## Architecture
|
|
821
|
+
{brief folder structure, max 10 lines}
|
|
822
|
+
|
|
823
|
+
## Conventions
|
|
824
|
+
{3-5 specific coding rules}
|
|
825
|
+
|
|
826
|
+
## Key Commands
|
|
827
|
+
{list /project: commands with descriptions}
|
|
828
|
+
|
|
829
|
+
## Output
|
|
830
|
+
{where results go, key files}
|
|
831
|
+
|
|
832
|
+
## Verification
|
|
833
|
+
After implementing any change, verify it works:
|
|
834
|
+
- {build command} \u2014 must pass with no errors
|
|
835
|
+
- {test command} \u2014 all tests must pass
|
|
836
|
+
- {lint command} \u2014 no warnings or errors
|
|
837
|
+
- {type check command} \u2014 no type errors
|
|
838
|
+
|
|
839
|
+
If any verification step fails, fix the issue before moving on.
|
|
840
|
+
Do NOT skip verification steps.
|
|
841
|
+
|
|
842
|
+
## Known Gotchas
|
|
843
|
+
<!-- After any correction, add it here: "Update CLAUDE.md so you don't make that mistake again." -->
|
|
844
|
+
<!-- Prune this section when it exceeds 10 items \u2014 keep only the recurring ones. -->
|
|
845
|
+
- (none yet \u2014 this section grows as you work)
|
|
846
|
+
|
|
847
|
+
## Debugging
|
|
848
|
+
When debugging, paste raw error output. Don't summarize \u2014 Claude works better with raw data.
|
|
849
|
+
Use subagents for deep investigation to keep main context clean.
|
|
850
|
+
|
|
851
|
+
## Git Workflow
|
|
852
|
+
- Prefer small, focused commits (one feature or fix per commit)
|
|
853
|
+
- Use conventional commits: feat:, fix:, docs:, refactor:, test:
|
|
854
|
+
- Target < 200 lines per PR when possible
|
|
855
|
+
\`\`\`
|
|
856
|
+
|
|
857
|
+
Do not add generic filler. Every line must be specific to the user's workflow.
|
|
858
|
+
|
|
859
|
+
## What You Must Always Include
|
|
860
|
+
|
|
861
|
+
1. A concise, workflow-specific \`claude_md\` (the CLAUDE.md content)
|
|
862
|
+
2. A \`/project:help\` command that explains the environment
|
|
863
|
+
3. A \`/project:tasks\` command for task management via TODO.md
|
|
864
|
+
4. A \`docs/TODO.md\` file for continuity
|
|
865
|
+
5. A \`docs/DECISIONS.md\` file for architectural decisions
|
|
866
|
+
6. A \`docs/LEARNINGS.md\` file for non-obvious discoveries
|
|
867
|
+
7. A \`rules/continuity.md\` rule encouraging updates to DECISIONS.md and LEARNINGS.md
|
|
868
|
+
8. A \`rules/security.md\` rule with essential security instructions
|
|
869
|
+
9. settings.json with deny rules for \`rm -rf\`, \`curl|sh\`, reading \`.env\` and \`secrets/\`
|
|
870
|
+
10. A \`/project:status\` command for code projects (uses ! for live git/test output)
|
|
871
|
+
11. A \`/project:fix\` command for code projects (uses $ARGUMENTS for issue number)
|
|
872
|
+
12. A \`docs/SPRINT.md\` file for sprint contracts (acceptance criteria, verification steps)
|
|
873
|
+
13. A "Verification" section in CLAUDE.md with concrete verify commands for the project
|
|
874
|
+
14. A "Known Gotchas" section in CLAUDE.md (starts empty, grows with corrections)
|
|
875
|
+
15. A "Debugging" section in CLAUDE.md (2 lines: paste raw errors, use subagents)
|
|
876
|
+
16. A "Git Workflow" section in CLAUDE.md (3 rules: small commits, conventional format, <200 lines PR)
|
|
877
|
+
|
|
878
|
+
## Tool Selection Rules
|
|
879
|
+
|
|
880
|
+
- Only select tools directly relevant to the described workflow
|
|
881
|
+
- Prefer free tools (auth: "none") when quality is comparable
|
|
882
|
+
- Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
|
|
883
|
+
- For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
|
|
884
|
+
- Maximum 6-8 MCP servers to avoid context bloat
|
|
885
|
+
- Include a \`reason\` for each selected tool explaining why it fits this workflow
|
|
886
|
+
|
|
887
|
+
## Context Budget (STRICT)
|
|
888
|
+
|
|
889
|
+
- MCP servers: maximum 6. Prefer fewer.
|
|
890
|
+
- CLAUDE.md: maximum 120 lines.
|
|
891
|
+
- Rules: maximum 5 files, each under 20 lines.
|
|
892
|
+
- Skills: maximum 3. Only include directly relevant ones.
|
|
893
|
+
- Agents: maximum 3. QA pipeline + one specialist.
|
|
894
|
+
- Commands: no limit (loaded on demand, zero context cost).
|
|
895
|
+
- Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
|
|
896
|
+
|
|
897
|
+
If the workflow doesn't clearly need a tool, DO NOT include it.
|
|
898
|
+
Each MCP server costs 500-2000 tokens of context window.
|
|
899
|
+
|
|
900
|
+
## Output Schema
|
|
901
|
+
|
|
902
|
+
Return ONLY valid JSON matching this structure:
|
|
903
|
+
|
|
660
904
|
\`\`\`json
|
|
661
905
|
{
|
|
662
906
|
"name": "short-kebab-case-name",
|
|
@@ -677,14 +921,7 @@ Return ONLY valid JSON matching this structure:
|
|
|
677
921
|
},
|
|
678
922
|
"commands": {
|
|
679
923
|
"help": "markdown content for /project:help",
|
|
680
|
-
"tasks": "markdown content for /project:tasks"
|
|
681
|
-
"status": "Show project status:\\n\\n!git status --short\\n\\n!git log --oneline -5\\n\\nRead TODO.md and summarize progress.",
|
|
682
|
-
"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",
|
|
683
|
-
"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.",
|
|
684
|
-
"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.",
|
|
685
|
-
"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.",
|
|
686
|
-
"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.",
|
|
687
|
-
"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."
|
|
924
|
+
"tasks": "markdown content for /project:tasks"
|
|
688
925
|
},
|
|
689
926
|
"rules": {
|
|
690
927
|
"continuity": "markdown content for continuity rule",
|
|
@@ -694,15 +931,13 @@ Return ONLY valid JSON matching this structure:
|
|
|
694
931
|
"skill-name/SKILL": "markdown content with YAML frontmatter"
|
|
695
932
|
},
|
|
696
933
|
"agents": {
|
|
697
|
-
"qa-orchestrator": "
|
|
698
|
-
"linter": "---\\nname: linter\\ndescription: Fast static analysis\\nmodel: haiku\\n---\\nRun available linters (eslint, prettier, biome, ruff, mypy, semgrep). Report issues.",
|
|
699
|
-
"e2e-tester": "---\\nname: e2e-tester\\ndescription: Browser-based QA via Playwright\\nmodel: sonnet\\n---\\nTest user flows via Playwright. Verify behavior, not just DOM. Screenshot failures."
|
|
934
|
+
"qa-orchestrator": "agent markdown with YAML frontmatter"
|
|
700
935
|
},
|
|
701
936
|
"docs": {
|
|
702
|
-
"TODO": "# TODO\\n\\n- [ ] First task
|
|
703
|
-
"DECISIONS": "# Decisions\\n\\nArchitectural decisions
|
|
704
|
-
"LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries
|
|
705
|
-
"SPRINT": "# Sprint Contract\\n\\nDefine acceptance criteria
|
|
937
|
+
"TODO": "# TODO\\n\\n- [ ] First task",
|
|
938
|
+
"DECISIONS": "# Decisions\\n\\nArchitectural decisions.",
|
|
939
|
+
"LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries.",
|
|
940
|
+
"SPRINT": "# Sprint Contract\\n\\nDefine acceptance criteria."
|
|
706
941
|
}
|
|
707
942
|
}
|
|
708
943
|
}
|
|
@@ -780,7 +1015,7 @@ async function loadRegistry() {
|
|
|
780
1015
|
}
|
|
781
1016
|
|
|
782
1017
|
// src/compiler/compile.ts
|
|
783
|
-
function
|
|
1018
|
+
function buildSkeletonMessage(intent, registry) {
|
|
784
1019
|
const registrySummary = registry.map(
|
|
785
1020
|
(t) => `- ${t.id} (${t.type}, tier ${t.tier}, auth: ${t.auth}): ${t.description} [best_for: ${t.best_for.join(", ")}]`
|
|
786
1021
|
).join("\n");
|
|
@@ -792,25 +1027,60 @@ ${intent}
|
|
|
792
1027
|
|
|
793
1028
|
${registrySummary}
|
|
794
1029
|
|
|
795
|
-
Generate the
|
|
1030
|
+
Generate the skeleton JSON now.`;
|
|
1031
|
+
}
|
|
1032
|
+
function buildHarnessMessage(intent, skeleton, concise) {
|
|
1033
|
+
const skeletonJson = JSON.stringify(skeleton, null, 2);
|
|
1034
|
+
const conciseNote = concise ? "\n\nIMPORTANT: Be concise. Maximum 80 lines for claude_md. Maximum 5 commands. Keep all content brief." : "";
|
|
1035
|
+
return `## User Intent
|
|
1036
|
+
|
|
1037
|
+
${intent}
|
|
1038
|
+
|
|
1039
|
+
## Project Skeleton
|
|
1040
|
+
|
|
1041
|
+
${skeletonJson}
|
|
1042
|
+
|
|
1043
|
+
Generate the harness content JSON now.${conciseNote}`;
|
|
796
1044
|
}
|
|
797
|
-
function
|
|
1045
|
+
function parseSkeletonResponse(text) {
|
|
798
1046
|
let cleaned = text.trim();
|
|
799
1047
|
if (cleaned.startsWith("```")) {
|
|
800
1048
|
cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
801
1049
|
}
|
|
802
1050
|
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
803
1051
|
if (!jsonMatch) {
|
|
1052
|
+
throw new Error("Pass 1 (skeleton) did not return valid JSON.");
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1056
|
+
if (!parsed.name || !parsed.tools || !Array.isArray(parsed.tools)) {
|
|
1057
|
+
throw new Error("Skeleton missing required fields: name, tools");
|
|
1058
|
+
}
|
|
1059
|
+
return parsed;
|
|
1060
|
+
} catch (err) {
|
|
804
1061
|
throw new Error(
|
|
805
|
-
|
|
1062
|
+
`Failed to parse skeleton JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
806
1063
|
);
|
|
807
1064
|
}
|
|
1065
|
+
}
|
|
1066
|
+
function parseHarnessResponse(text) {
|
|
1067
|
+
let cleaned = text.trim();
|
|
1068
|
+
if (cleaned.startsWith("```")) {
|
|
1069
|
+
cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
1070
|
+
}
|
|
1071
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
1072
|
+
if (!jsonMatch) {
|
|
1073
|
+
throw new Error("Pass 2 (harness) did not return valid JSON.");
|
|
1074
|
+
}
|
|
808
1075
|
try {
|
|
809
|
-
|
|
1076
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1077
|
+
if (!parsed.claude_md || !parsed.commands) {
|
|
1078
|
+
throw new Error("Harness missing required fields: claude_md, commands");
|
|
1079
|
+
}
|
|
1080
|
+
return parsed;
|
|
810
1081
|
} catch (err) {
|
|
811
1082
|
throw new Error(
|
|
812
|
-
`Failed to parse
|
|
813
|
-
Response started with: ${cleaned.slice(0, 200)}...`
|
|
1083
|
+
`Failed to parse harness JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
814
1084
|
);
|
|
815
1085
|
}
|
|
816
1086
|
}
|
|
@@ -844,14 +1114,17 @@ function classifyError(err, provider) {
|
|
|
844
1114
|
}
|
|
845
1115
|
return `${provider} API error: ${msg}`;
|
|
846
1116
|
}
|
|
847
|
-
async function callLLM(config, userMessage) {
|
|
1117
|
+
async function callLLM(config, userMessage, options) {
|
|
1118
|
+
const maxTokens = options?.maxTokens ?? 8192;
|
|
1119
|
+
const systemPrompt = options?.systemPrompt ?? SYSTEM_PROMPT;
|
|
1120
|
+
const providerName = getProviderName(config.provider);
|
|
848
1121
|
if (config.provider === "anthropic") {
|
|
849
|
-
const
|
|
1122
|
+
const client2 = new Anthropic2({ apiKey: config.api_key });
|
|
850
1123
|
try {
|
|
851
|
-
const response = await
|
|
1124
|
+
const response = await client2.messages.create({
|
|
852
1125
|
model: config.model,
|
|
853
|
-
max_tokens:
|
|
854
|
-
system:
|
|
1126
|
+
max_tokens: maxTokens,
|
|
1127
|
+
system: systemPrompt,
|
|
855
1128
|
messages: [{ role: "user", content: userMessage }]
|
|
856
1129
|
});
|
|
857
1130
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
@@ -860,34 +1133,90 @@ async function callLLM(config, userMessage) {
|
|
|
860
1133
|
}
|
|
861
1134
|
return textBlock.text;
|
|
862
1135
|
} catch (err) {
|
|
863
|
-
throw new Error(classifyError(err,
|
|
1136
|
+
throw new Error(classifyError(err, providerName));
|
|
864
1137
|
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1138
|
+
}
|
|
1139
|
+
const resolvedBaseURL = getBaseURL(config.provider, config.base_url);
|
|
1140
|
+
const clientOptions = { apiKey: config.api_key };
|
|
1141
|
+
if (resolvedBaseURL) clientOptions.baseURL = resolvedBaseURL;
|
|
1142
|
+
const client = new OpenAI2(clientOptions);
|
|
1143
|
+
try {
|
|
1144
|
+
const response = await client.chat.completions.create({
|
|
1145
|
+
model: config.model,
|
|
1146
|
+
max_tokens: maxTokens,
|
|
1147
|
+
messages: [
|
|
1148
|
+
{ role: "system", content: systemPrompt },
|
|
1149
|
+
{ role: "user", content: userMessage }
|
|
1150
|
+
]
|
|
1151
|
+
});
|
|
1152
|
+
const text = response.choices[0]?.message?.content;
|
|
1153
|
+
if (!text) {
|
|
1154
|
+
throw new Error("No text response from compiler LLM");
|
|
870
1155
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1156
|
+
return text;
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
throw new Error(classifyError(err, providerName));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function buildSettings(skeleton, registry) {
|
|
1162
|
+
const selectedTools = skeleton.tools.map((t) => registry.find((r) => r.id === t.tool_id)).filter(Boolean);
|
|
1163
|
+
const allow = ["Read", "Write", "Edit", "Bash(npm run *)", "Bash(npx *)"];
|
|
1164
|
+
const deny = [
|
|
1165
|
+
"Bash(rm -rf *)",
|
|
1166
|
+
"Bash(curl * | sh)",
|
|
1167
|
+
"Bash(wget * | sh)",
|
|
1168
|
+
"Read(./.env)",
|
|
1169
|
+
"Read(./secrets/**)"
|
|
1170
|
+
];
|
|
1171
|
+
const hooks = {
|
|
1172
|
+
PreToolUse: [
|
|
1173
|
+
{
|
|
1174
|
+
matcher: "Bash",
|
|
1175
|
+
hooks: [
|
|
1176
|
+
{
|
|
1177
|
+
type: "command",
|
|
1178
|
+
command: `CMD=$(cat | jq -r '.tool_input.command // empty') && echo "$CMD" | grep -qiE 'rm\\s+-rf\\s+/|DROP\\s+TABLE|curl.*\\|\\s*sh' && echo 'Blocked destructive command' >&2 && exit 2 || true`
|
|
1179
|
+
}
|
|
879
1180
|
]
|
|
880
|
-
});
|
|
881
|
-
const text = response.choices[0]?.message?.content;
|
|
882
|
-
if (!text) {
|
|
883
|
-
throw new Error("No text response from compiler LLM");
|
|
884
1181
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1182
|
+
],
|
|
1183
|
+
PostCompact: [
|
|
1184
|
+
{
|
|
1185
|
+
matcher: "",
|
|
1186
|
+
hooks: [
|
|
1187
|
+
{
|
|
1188
|
+
type: "prompt",
|
|
1189
|
+
prompt: "Re-read CLAUDE.md and docs/SPRINT.md (if it exists) to restore project context after compaction."
|
|
1190
|
+
}
|
|
1191
|
+
]
|
|
1192
|
+
}
|
|
1193
|
+
]
|
|
1194
|
+
};
|
|
1195
|
+
const techStack = skeleton.outline.tech_stack.map((t) => t.toLowerCase());
|
|
1196
|
+
if (techStack.some((t) => t.includes("typescript") || t.includes("javascript") || t.includes("react") || t.includes("next"))) {
|
|
1197
|
+
hooks.PostToolUse = [
|
|
1198
|
+
{
|
|
1199
|
+
matcher: "Edit|Write",
|
|
1200
|
+
hooks: [
|
|
1201
|
+
{
|
|
1202
|
+
type: "command",
|
|
1203
|
+
command: `FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n "$FILE" ] && npx prettier --write "$FILE" 2>/dev/null || true`
|
|
1204
|
+
}
|
|
1205
|
+
]
|
|
1206
|
+
}
|
|
1207
|
+
];
|
|
1208
|
+
}
|
|
1209
|
+
return { permissions: { allow, deny }, hooks };
|
|
1210
|
+
}
|
|
1211
|
+
function buildMcpConfig(skeleton, registry) {
|
|
1212
|
+
const config = {};
|
|
1213
|
+
for (const tool of skeleton.tools) {
|
|
1214
|
+
const reg = registry.find((r) => r.id === tool.tool_id);
|
|
1215
|
+
if (reg?.install.mcp_config) {
|
|
1216
|
+
config[tool.tool_id] = reg.install.mcp_config;
|
|
888
1217
|
}
|
|
889
1218
|
}
|
|
890
|
-
|
|
1219
|
+
return config;
|
|
891
1220
|
}
|
|
892
1221
|
function validateSpec(spec, onProgress) {
|
|
893
1222
|
const warnings = [];
|
|
@@ -914,17 +1243,52 @@ async function compile(intent, onProgress) {
|
|
|
914
1243
|
}
|
|
915
1244
|
onProgress?.("Loading tool registry...");
|
|
916
1245
|
const registry = await loadRegistry();
|
|
917
|
-
onProgress?.(
|
|
918
|
-
const
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
1246
|
+
onProgress?.("Analyzing workflow...");
|
|
1247
|
+
const skeletonMsg = buildSkeletonMessage(intent, registry);
|
|
1248
|
+
const skeletonText = await callLLM(config, skeletonMsg, {
|
|
1249
|
+
maxTokens: 2048,
|
|
1250
|
+
systemPrompt: SKELETON_PROMPT
|
|
1251
|
+
});
|
|
1252
|
+
const skeleton = parseSkeletonResponse(skeletonText);
|
|
1253
|
+
onProgress?.("Generating environment...");
|
|
1254
|
+
const harnessMsg = buildHarnessMessage(intent, skeleton);
|
|
1255
|
+
let harness;
|
|
1256
|
+
try {
|
|
1257
|
+
const harnessText = await callLLM(config, harnessMsg, {
|
|
1258
|
+
maxTokens: 8192,
|
|
1259
|
+
systemPrompt: HARNESS_PROMPT
|
|
1260
|
+
});
|
|
1261
|
+
harness = parseHarnessResponse(harnessText);
|
|
1262
|
+
} catch {
|
|
1263
|
+
onProgress?.("Retrying with concise mode...");
|
|
1264
|
+
const retryMsg = buildHarnessMessage(intent, skeleton, true);
|
|
1265
|
+
const retryText = await callLLM(config, retryMsg, {
|
|
1266
|
+
maxTokens: 8192,
|
|
1267
|
+
systemPrompt: HARNESS_PROMPT
|
|
1268
|
+
});
|
|
1269
|
+
harness = parseHarnessResponse(retryText);
|
|
1270
|
+
}
|
|
1271
|
+
onProgress?.("Configuring tools...");
|
|
1272
|
+
const settings = buildSettings(skeleton, registry);
|
|
1273
|
+
const mcpConfig = buildMcpConfig(skeleton, registry);
|
|
922
1274
|
const spec = {
|
|
923
1275
|
id: `env_${crypto.randomUUID()}`,
|
|
924
1276
|
intent,
|
|
925
1277
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
926
|
-
|
|
927
|
-
|
|
1278
|
+
name: skeleton.name,
|
|
1279
|
+
description: skeleton.description,
|
|
1280
|
+
autonomy_level: 1,
|
|
1281
|
+
tools: skeleton.tools,
|
|
1282
|
+
harness: {
|
|
1283
|
+
claude_md: harness.claude_md,
|
|
1284
|
+
settings,
|
|
1285
|
+
mcp_config: mcpConfig,
|
|
1286
|
+
commands: harness.commands,
|
|
1287
|
+
rules: harness.rules,
|
|
1288
|
+
skills: harness.skills ?? {},
|
|
1289
|
+
agents: harness.agents ?? {},
|
|
1290
|
+
docs: harness.docs
|
|
1291
|
+
}
|
|
928
1292
|
};
|
|
929
1293
|
validateSpec(spec, onProgress);
|
|
930
1294
|
await ensureDirs();
|
|
@@ -939,9 +1303,7 @@ async function generateClarifications(intent, onProgress) {
|
|
|
939
1303
|
}
|
|
940
1304
|
onProgress?.("Analyzing your request...");
|
|
941
1305
|
const clarificationConfig = { ...config };
|
|
942
|
-
|
|
943
|
-
clarificationConfig.model = "claude-haiku-4-5-20251001";
|
|
944
|
-
}
|
|
1306
|
+
clarificationConfig.model = getCheapModel(config.provider, config.model);
|
|
945
1307
|
const response = await callLLM(clarificationConfig, CLARIFICATION_PROMPT + "\n\nUser description: " + intent);
|
|
946
1308
|
try {
|
|
947
1309
|
let cleaned = response.trim();
|
|
@@ -1653,7 +2015,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
|
|
|
1653
2015
|
);
|
|
1654
2016
|
process.exit(1);
|
|
1655
2017
|
}
|
|
1656
|
-
const intentRaw = intentArg || await
|
|
2018
|
+
const intentRaw = intentArg || await input2({
|
|
1657
2019
|
message: "What do you want your agent to do?"
|
|
1658
2020
|
});
|
|
1659
2021
|
if (!intentRaw.trim()) {
|
|
@@ -1673,7 +2035,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
|
|
|
1673
2035
|
if (clarifications.length > 0) {
|
|
1674
2036
|
const answers = [];
|
|
1675
2037
|
for (const c of clarifications) {
|
|
1676
|
-
const answer = await
|
|
2038
|
+
const answer = await input2({
|
|
1677
2039
|
message: c.question,
|
|
1678
2040
|
default: c.suggestion
|
|
1679
2041
|
});
|
|
@@ -2425,7 +2787,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
|
|
|
2425
2787
|
console.log(ui.file(file));
|
|
2426
2788
|
}
|
|
2427
2789
|
if (hasEnvVars) {
|
|
2428
|
-
await collectAndWriteKeys(summary.envSetup, targetDir
|
|
2790
|
+
await collectAndWriteKeys(summary.envSetup, targetDir);
|
|
2429
2791
|
console.log("");
|
|
2430
2792
|
}
|
|
2431
2793
|
if (summary.pluginCommands.length > 0) {
|
|
@@ -2619,7 +2981,7 @@ var doctorCommand = new Command7("doctor").description(
|
|
|
2619
2981
|
// src/commands/registry.ts
|
|
2620
2982
|
import { Command as Command8 } from "commander";
|
|
2621
2983
|
import chalk11 from "chalk";
|
|
2622
|
-
import { input as
|
|
2984
|
+
import { input as input3, select as select3 } from "@inquirer/prompts";
|
|
2623
2985
|
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
2986
|
printCompactBanner();
|
|
2625
2987
|
let all;
|
|
@@ -2679,7 +3041,7 @@ var listCommand2 = new Command8("list").description("List tools in the registry"
|
|
|
2679
3041
|
var addCommand = new Command8("add").description("Add a tool to the user registry").action(async () => {
|
|
2680
3042
|
let id;
|
|
2681
3043
|
try {
|
|
2682
|
-
id = await
|
|
3044
|
+
id = await input3({
|
|
2683
3045
|
message: "Tool ID (kebab-case)",
|
|
2684
3046
|
validate: (v) => {
|
|
2685
3047
|
if (!v) return "ID is required";
|
|
@@ -2687,8 +3049,8 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
|
|
|
2687
3049
|
return true;
|
|
2688
3050
|
}
|
|
2689
3051
|
});
|
|
2690
|
-
const name = await
|
|
2691
|
-
const description = await
|
|
3052
|
+
const name = await input3({ message: "Display name" });
|
|
3053
|
+
const description = await input3({ message: "Description" });
|
|
2692
3054
|
const category = await select3({
|
|
2693
3055
|
message: "Category",
|
|
2694
3056
|
choices: [
|
|
@@ -2732,8 +3094,8 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
|
|
|
2732
3094
|
if (auth === "api_key" || auth === "connection_string") {
|
|
2733
3095
|
let addMore = true;
|
|
2734
3096
|
while (addMore) {
|
|
2735
|
-
const varName = await
|
|
2736
|
-
const varDesc = await
|
|
3097
|
+
const varName = await input3({ message: "Env var name" });
|
|
3098
|
+
const varDesc = await input3({ message: "Env var description" });
|
|
2737
3099
|
env_vars.push({ name: varName, description: varDesc });
|
|
2738
3100
|
const another = await select3({
|
|
2739
3101
|
message: "Add another env var?",
|
|
@@ -2745,14 +3107,14 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
|
|
|
2745
3107
|
addMore = another;
|
|
2746
3108
|
}
|
|
2747
3109
|
}
|
|
2748
|
-
const signup_url_raw = await
|
|
3110
|
+
const signup_url_raw = await input3({ message: "Signup URL (optional, press enter to skip)" });
|
|
2749
3111
|
const signup_url = signup_url_raw.trim() || void 0;
|
|
2750
|
-
const best_for_raw = await
|
|
3112
|
+
const best_for_raw = await input3({ message: "Best-for tags, comma-separated" });
|
|
2751
3113
|
const best_for = best_for_raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2752
3114
|
const install = {};
|
|
2753
3115
|
if (type === "mcp_server") {
|
|
2754
|
-
const command = await
|
|
2755
|
-
const args_raw = await
|
|
3116
|
+
const command = await input3({ message: "MCP command" });
|
|
3117
|
+
const args_raw = await input3({ message: "MCP args, comma-separated (leave blank for none)" });
|
|
2756
3118
|
const args = args_raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2757
3119
|
install.mcp_config = { command, args };
|
|
2758
3120
|
}
|