@kitnai/cli 0.1.30 → 0.1.31

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/index.js CHANGED
@@ -240,7 +240,8 @@ var init_config = __esm({
240
240
  storage: z.string(),
241
241
  crons: z.string().optional()
242
242
  }),
243
- registries: z.record(z.string(), registryValueSchema)
243
+ registries: z.record(z.string(), registryValueSchema),
244
+ aiTools: z.array(z.string()).optional()
244
245
  });
245
246
  FRAMEWORK_TO_ADAPTER = {
246
247
  hono: "hono",
@@ -1421,6 +1422,145 @@ var init_add = __esm({
1421
1422
  }
1422
1423
  });
1423
1424
 
1425
+ // src/installers/rules-generator.ts
1426
+ import { writeFile as writeFile7, mkdir as mkdir4 } from "fs/promises";
1427
+ import { join as join8, dirname as dirname4 } from "path";
1428
+ function deriveRulesBaseUrl(registries) {
1429
+ const kitnEntry = registries["@kitn"];
1430
+ if (!kitnEntry) {
1431
+ throw new Error("No @kitn registry configured");
1432
+ }
1433
+ const url = getRegistryUrl(kitnEntry);
1434
+ return url.replace("{type}/{name}.json", "rules/");
1435
+ }
1436
+ async function fetchRulesConfig(registries) {
1437
+ try {
1438
+ const baseUrl = deriveRulesBaseUrl(registries);
1439
+ const res = await fetch(baseUrl + "config.json");
1440
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1441
+ return await res.json();
1442
+ } catch {
1443
+ return FALLBACK_CONFIG;
1444
+ }
1445
+ }
1446
+ async function fetchRulesTemplate(registries) {
1447
+ try {
1448
+ const baseUrl = deriveRulesBaseUrl(registries);
1449
+ const res = await fetch(baseUrl + "template.md");
1450
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1451
+ return await res.text();
1452
+ } catch {
1453
+ return FALLBACK_TEMPLATE;
1454
+ }
1455
+ }
1456
+ function renderTemplate(template, aliases) {
1457
+ const base = aliases.base ?? "src/ai";
1458
+ return template.replace(/\{base\}/g, base).replace(/\{agents\}/g, aliases.agents).replace(/\{tools\}/g, aliases.tools).replace(/\{skills\}/g, aliases.skills).replace(/\{storage\}/g, aliases.storage).replace(/\{crons\}/g, aliases.crons ?? `${base}/crons`);
1459
+ }
1460
+ function wrapContent(content, tool) {
1461
+ if (tool.format === "mdc" && tool.frontmatter) {
1462
+ const lines = Object.entries(tool.frontmatter).map(
1463
+ ([key, value]) => `${key}: ${value}`
1464
+ );
1465
+ return `---
1466
+ ${lines.join("\n")}
1467
+ ---
1468
+
1469
+ ${content}`;
1470
+ }
1471
+ return content;
1472
+ }
1473
+ async function generateRulesFiles(cwd, config, selectedToolIds) {
1474
+ const rulesConfig = await fetchRulesConfig(config.registries);
1475
+ const template = await fetchRulesTemplate(config.registries);
1476
+ const rendered = renderTemplate(template, config.aliases);
1477
+ const toolsToWrite = selectedToolIds ? rulesConfig.tools.filter((t) => selectedToolIds.includes(t.id)) : rulesConfig.tools;
1478
+ const written = [];
1479
+ for (const tool of toolsToWrite) {
1480
+ const content = wrapContent(rendered, tool);
1481
+ const filePath = join8(cwd, tool.filePath);
1482
+ await mkdir4(dirname4(filePath), { recursive: true });
1483
+ await writeFile7(filePath, content);
1484
+ written.push(tool.filePath);
1485
+ }
1486
+ return written;
1487
+ }
1488
+ var FALLBACK_CONFIG, FALLBACK_TEMPLATE;
1489
+ var init_rules_generator = __esm({
1490
+ "src/installers/rules-generator.ts"() {
1491
+ "use strict";
1492
+ init_config();
1493
+ FALLBACK_CONFIG = {
1494
+ version: "1.0.0",
1495
+ tools: [
1496
+ {
1497
+ id: "claude-code",
1498
+ name: "Claude Code",
1499
+ filePath: "AGENTS.md",
1500
+ format: "plain",
1501
+ description: "Also works with any tool that reads AGENTS.md"
1502
+ },
1503
+ {
1504
+ id: "cursor",
1505
+ name: "Cursor",
1506
+ filePath: ".cursor/rules/kitn.mdc",
1507
+ format: "mdc",
1508
+ frontmatter: {
1509
+ description: "kitn AI agent framework conventions and patterns",
1510
+ globs: "src/ai/**/*.ts, kitn.json"
1511
+ }
1512
+ },
1513
+ {
1514
+ id: "github-copilot",
1515
+ name: "GitHub Copilot",
1516
+ filePath: ".github/copilot-instructions.md",
1517
+ format: "plain"
1518
+ },
1519
+ {
1520
+ id: "cline",
1521
+ name: "Cline",
1522
+ filePath: ".clinerules",
1523
+ format: "plain"
1524
+ },
1525
+ {
1526
+ id: "windsurf",
1527
+ name: "Windsurf",
1528
+ filePath: ".windsurfrules",
1529
+ format: "plain"
1530
+ }
1531
+ ]
1532
+ };
1533
+ FALLBACK_TEMPLATE = `# kitn AI Agent Framework
1534
+
1535
+ This project uses **kitn** to build multi-agent AI systems.
1536
+
1537
+ ## Project Structure
1538
+
1539
+ AI components live under \`{base}\`:
1540
+
1541
+ - \`{agents}/\` \u2014 Agent definitions
1542
+ - \`{tools}/\` \u2014 Tool definitions
1543
+ - \`{skills}/\` \u2014 Skill files (markdown)
1544
+ - \`{storage}/\` \u2014 Storage providers
1545
+ - \`{crons}/\` \u2014 Cron job definitions
1546
+
1547
+ ## Patterns
1548
+
1549
+ - Agents: \`registerAgent({ name, system, tools })\` from \`@kitn/core\`
1550
+ - Tools: \`tool()\` from \`ai\` + \`registerTool()\` from \`@kitn/core\`
1551
+ - Always use \`.js\` extension in relative imports
1552
+ - Use \`@kitn/core\` for core imports, \`ai\` for Vercel AI SDK
1553
+
1554
+ ## CLI
1555
+
1556
+ - \`kitn add <name>\` \u2014 install from registry
1557
+ - \`kitn create <type> <name>\` \u2014 scaffold locally
1558
+ - \`kitn link tool <name> --to <agent>\` \u2014 wire a tool to an agent
1559
+ - \`kitn list\` \u2014 browse components
1560
+ `;
1561
+ }
1562
+ });
1563
+
1424
1564
  // src/commands/init.ts
1425
1565
  var init_exports = {};
1426
1566
  __export(init_exports, {
@@ -1428,8 +1568,8 @@ __export(init_exports, {
1428
1568
  });
1429
1569
  import * as p3 from "@clack/prompts";
1430
1570
  import pc4 from "picocolors";
1431
- import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
1432
- import { join as join8 } from "path";
1571
+ import { mkdir as mkdir5, writeFile as writeFile8 } from "fs/promises";
1572
+ import { join as join9 } from "path";
1433
1573
  function getPluginTemplate(framework) {
1434
1574
  const adapterName = framework === "hono-openapi" ? "hono-openapi" : framework;
1435
1575
  return `import { createAIPlugin } from "@kitn/adapters/${adapterName}";
@@ -1567,13 +1707,45 @@ async function initCommand(opts = {}) {
1567
1707
  p3.log.info(`Patched tsconfig.json with path: ${pc4.bold("@kitn/*")}`);
1568
1708
  p3.log.info("Installing core engine and adapter...");
1569
1709
  await addCommand(["core", "routes"], { overwrite: true });
1570
- const aiDir = join8(cwd, baseDir);
1571
- await mkdir4(aiDir, { recursive: true });
1572
- const barrelPath = join8(aiDir, "index.ts");
1573
- await writeFile7(barrelPath, createBarrelFile());
1574
- const pluginPath = join8(aiDir, "plugin.ts");
1575
- await writeFile7(pluginPath, getPluginTemplate(framework));
1710
+ const aiDir = join9(cwd, baseDir);
1711
+ await mkdir5(aiDir, { recursive: true });
1712
+ const barrelPath = join9(aiDir, "index.ts");
1713
+ await writeFile8(barrelPath, createBarrelFile());
1714
+ const pluginPath = join9(aiDir, "plugin.ts");
1715
+ await writeFile8(pluginPath, getPluginTemplate(framework));
1576
1716
  p3.log.success(`Created ${pc4.bold(baseDir + "/plugin.ts")} \u2014 configure your AI provider there`);
1717
+ try {
1718
+ const rulesConfig = await fetchRulesConfig(config.registries);
1719
+ let selectedToolIds;
1720
+ if (opts.yes) {
1721
+ selectedToolIds = rulesConfig.tools.map((t) => t.id);
1722
+ } else {
1723
+ const selected = await p3.multiselect({
1724
+ message: "Which AI coding tools do you use?",
1725
+ options: rulesConfig.tools.map((t) => ({
1726
+ value: t.id,
1727
+ label: t.name,
1728
+ hint: t.description
1729
+ })),
1730
+ required: false
1731
+ });
1732
+ if (p3.isCancel(selected)) {
1733
+ selectedToolIds = [];
1734
+ } else {
1735
+ selectedToolIds = selected;
1736
+ }
1737
+ }
1738
+ if (selectedToolIds.length > 0) {
1739
+ const updatedConfig = { ...config, aiTools: selectedToolIds };
1740
+ await writeConfig(cwd, updatedConfig);
1741
+ const written = await generateRulesFiles(cwd, updatedConfig, selectedToolIds);
1742
+ for (const filePath of written) {
1743
+ p3.log.success(`Created ${pc4.bold(filePath)}`);
1744
+ }
1745
+ }
1746
+ } catch {
1747
+ p3.log.warn("Could not generate AI coding tool rules (non-fatal).");
1748
+ }
1577
1749
  const mountCode = framework === "elysia" ? `app.use(new Elysia({ prefix: "/api" }).use(ai.router));` : `app.route("/api", ai.router);`;
1578
1750
  p3.note(
1579
1751
  [
@@ -1601,6 +1773,7 @@ var init_init = __esm({
1601
1773
  init_tsconfig_patcher();
1602
1774
  init_barrel_manager();
1603
1775
  init_add();
1776
+ init_rules_generator();
1604
1777
  }
1605
1778
  });
1606
1779
 
@@ -1736,7 +1909,7 @@ __export(diff_exports, {
1736
1909
  diffCommand: () => diffCommand
1737
1910
  });
1738
1911
  import * as p5 from "@clack/prompts";
1739
- import { join as join9 } from "path";
1912
+ import { join as join10 } from "path";
1740
1913
  async function diffCommand(componentName) {
1741
1914
  const cwd = process.cwd();
1742
1915
  const config = await readConfig(cwd);
@@ -1767,8 +1940,8 @@ async function diffCommand(componentName) {
1767
1940
  for (const file of registryItem.files) {
1768
1941
  if (indexItem.type === "kitn:package") {
1769
1942
  const baseDir = config.aliases.base ?? "src/ai";
1770
- const localPath = join9(cwd, baseDir, file.path);
1771
- const relativePath = join9(baseDir, file.path);
1943
+ const localPath = join10(cwd, baseDir, file.path);
1944
+ const relativePath = join10(baseDir, file.path);
1772
1945
  const localContent = await readExistingFile(localPath);
1773
1946
  if (localContent === null) {
1774
1947
  p5.log.warn(`${relativePath}: file missing locally`);
@@ -1792,7 +1965,7 @@ async function diffCommand(componentName) {
1792
1965
  return "storage";
1793
1966
  }
1794
1967
  })();
1795
- const localPath = join9(cwd, config.aliases[aliasKey], fileName);
1968
+ const localPath = join10(cwd, config.aliases[aliasKey], fileName);
1796
1969
  const localContent = await readExistingFile(localPath);
1797
1970
  if (localContent === null) {
1798
1971
  p5.log.warn(`${fileName}: file missing locally`);
@@ -1826,8 +1999,8 @@ __export(remove_exports, {
1826
1999
  });
1827
2000
  import * as p6 from "@clack/prompts";
1828
2001
  import pc6 from "picocolors";
1829
- import { join as join10, relative as relative3, dirname as dirname4 } from "path";
1830
- import { unlink as unlink3, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
2002
+ import { join as join11, relative as relative3, dirname as dirname5 } from "path";
2003
+ import { unlink as unlink3, readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
1831
2004
  import { existsSync as existsSync2 } from "fs";
1832
2005
  async function removeSingleComponent(installedKey, lock, config, cwd) {
1833
2006
  const entry = lock[installedKey];
@@ -1835,15 +2008,15 @@ async function removeSingleComponent(installedKey, lock, config, cwd) {
1835
2008
  const deleted = [];
1836
2009
  for (const filePath of entry.files) {
1837
2010
  try {
1838
- await unlink3(join10(cwd, filePath));
2011
+ await unlink3(join11(cwd, filePath));
1839
2012
  deleted.push(filePath);
1840
2013
  } catch {
1841
2014
  p6.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
1842
2015
  }
1843
2016
  }
1844
2017
  const baseDir = config.aliases.base ?? "src/ai";
1845
- const barrelPath = join10(cwd, baseDir, "index.ts");
1846
- const barrelDir = join10(cwd, baseDir);
2018
+ const barrelPath = join11(cwd, baseDir, "index.ts");
2019
+ const barrelDir = join11(cwd, baseDir);
1847
2020
  const barrelEligibleDirs = /* @__PURE__ */ new Set([
1848
2021
  config.aliases.agents,
1849
2022
  config.aliases.tools,
@@ -1853,9 +2026,9 @@ async function removeSingleComponent(installedKey, lock, config, cwd) {
1853
2026
  let barrelContent = await readFile7(barrelPath, "utf-8");
1854
2027
  let barrelChanged = false;
1855
2028
  for (const filePath of deleted) {
1856
- const fileDir = dirname4(filePath);
2029
+ const fileDir = dirname5(filePath);
1857
2030
  if (!barrelEligibleDirs.has(fileDir)) continue;
1858
- const importPath = "./" + relative3(barrelDir, join10(cwd, filePath)).replace(/\\/g, "/");
2031
+ const importPath = "./" + relative3(barrelDir, join11(cwd, filePath)).replace(/\\/g, "/");
1859
2032
  const updated = removeImportFromBarrel(barrelContent, importPath);
1860
2033
  if (updated !== barrelContent) {
1861
2034
  barrelContent = updated;
@@ -1863,8 +2036,8 @@ async function removeSingleComponent(installedKey, lock, config, cwd) {
1863
2036
  }
1864
2037
  }
1865
2038
  if (barrelChanged) {
1866
- await writeFile8(barrelPath, barrelContent);
1867
- p6.log.info(`Updated barrel file: ${join10(baseDir, "index.ts")}`);
2039
+ await writeFile9(barrelPath, barrelContent);
2040
+ p6.log.info(`Updated barrel file: ${join11(baseDir, "index.ts")}`);
1868
2041
  }
1869
2042
  }
1870
2043
  delete lock[installedKey];
@@ -2012,6 +2185,19 @@ var init_update = __esm({
2012
2185
  }
2013
2186
  });
2014
2187
 
2188
+ // src/utils/naming.ts
2189
+ function toCamelCase(str) {
2190
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2191
+ }
2192
+ function toTitleCase(str) {
2193
+ return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2194
+ }
2195
+ var init_naming = __esm({
2196
+ "src/utils/naming.ts"() {
2197
+ "use strict";
2198
+ }
2199
+ });
2200
+
2015
2201
  // src/commands/create.ts
2016
2202
  var create_exports = {};
2017
2203
  __export(create_exports, {
@@ -2020,15 +2206,9 @@ __export(create_exports, {
2020
2206
  });
2021
2207
  import * as p8 from "@clack/prompts";
2022
2208
  import pc7 from "picocolors";
2023
- import { join as join11, relative as relative4 } from "path";
2209
+ import { join as join12, relative as relative4 } from "path";
2024
2210
  import { existsSync as existsSync3 } from "fs";
2025
- import { readFile as readFile8, writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
2026
- function toCamelCase(str) {
2027
- return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2028
- }
2029
- function toTitleCase(str) {
2030
- return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2031
- }
2211
+ import { readFile as readFile8, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
2032
2212
  function generateAgentSource(name) {
2033
2213
  const camel = toCamelCase(name);
2034
2214
  return `import { registerAgent } from "@kitn/core";
@@ -2135,7 +2315,7 @@ async function createComponentInProject(type, name, opts) {
2135
2315
  const validType = type;
2136
2316
  const kitnType = typeToKitnType[validType];
2137
2317
  const fileName = validType === "skill" ? `${name}.md` : `${name}.ts`;
2138
- const filePath = join11(cwd, getInstallPath(config, kitnType, fileName));
2318
+ const filePath = join12(cwd, getInstallPath(config, kitnType, fileName));
2139
2319
  const dummyContent = "";
2140
2320
  const status = await checkFileStatus(filePath, dummyContent);
2141
2321
  if (status !== "new" /* New */) {
@@ -2163,18 +2343,18 @@ async function createComponentInProject(type, name, opts) {
2163
2343
  let barrelUpdated = false;
2164
2344
  if (BARREL_TYPES.includes(validType)) {
2165
2345
  const baseDir = config.aliases.base ?? "src/ai";
2166
- const barrelPath = join11(cwd, baseDir, "index.ts");
2346
+ const barrelPath = join12(cwd, baseDir, "index.ts");
2167
2347
  let barrelContent;
2168
2348
  if (existsSync3(barrelPath)) {
2169
2349
  barrelContent = await readFile8(barrelPath, "utf-8");
2170
2350
  } else {
2171
2351
  barrelContent = createBarrelFile();
2172
- await mkdir5(join11(cwd, baseDir), { recursive: true });
2352
+ await mkdir6(join12(cwd, baseDir), { recursive: true });
2173
2353
  }
2174
- const importPath = "./" + relative4(join11(cwd, baseDir), filePath).replace(/\.ts$/, ".js");
2354
+ const importPath = "./" + relative4(join12(cwd, baseDir), filePath).replace(/\.ts$/, ".js");
2175
2355
  const updatedBarrel = addImportToBarrel(barrelContent, importPath);
2176
2356
  if (updatedBarrel !== barrelContent) {
2177
- await writeFile9(barrelPath, updatedBarrel);
2357
+ await writeFile10(barrelPath, updatedBarrel);
2178
2358
  barrelUpdated = true;
2179
2359
  }
2180
2360
  }
@@ -2202,6 +2382,7 @@ var init_create = __esm({
2202
2382
  "src/commands/create.ts"() {
2203
2383
  "use strict";
2204
2384
  init_config();
2385
+ init_naming();
2205
2386
  init_file_writer();
2206
2387
  init_barrel_manager();
2207
2388
  VALID_TYPES = ["agent", "tool", "skill", "storage", "cron"];
@@ -2216,36 +2397,661 @@ var init_create = __esm({
2216
2397
  }
2217
2398
  });
2218
2399
 
2400
+ // src/utils/component-resolver.ts
2401
+ import { readdir, readFile as readFile9 } from "fs/promises";
2402
+ import { join as join13, relative as relative5 } from "path";
2403
+ function stripSuffix(name, suffix) {
2404
+ if (name.endsWith(`-${suffix}`)) {
2405
+ return name.slice(0, -(suffix.length + 1));
2406
+ }
2407
+ return name;
2408
+ }
2409
+ function toolsDir(config, cwd) {
2410
+ const baseAlias = config.aliases.base ?? "src/ai";
2411
+ const tools = config.aliases.tools ?? join13(baseAlias, "tools");
2412
+ return join13(cwd, tools);
2413
+ }
2414
+ function agentsDir(config, cwd) {
2415
+ const baseAlias = config.aliases.base ?? "src/ai";
2416
+ const agents = config.aliases.agents ?? join13(baseAlias, "agents");
2417
+ return join13(cwd, agents);
2418
+ }
2419
+ async function findFile(dir, candidates) {
2420
+ for (const name of candidates) {
2421
+ const filePath = join13(dir, `${name}.ts`);
2422
+ try {
2423
+ await readFile9(filePath);
2424
+ return filePath;
2425
+ } catch {
2426
+ }
2427
+ }
2428
+ return null;
2429
+ }
2430
+ function parseExportName(source) {
2431
+ const match = source.match(/export\s+const\s+(\w+)/);
2432
+ return match ? match[1] : null;
2433
+ }
2434
+ function parseAgentName(source) {
2435
+ const match = source.match(/registerAgent\s*\(\s*\{[^}]*name:\s*"([^"]+)"/s);
2436
+ return match ? match[1] : null;
2437
+ }
2438
+ function computeImportPath(fromDir, toFile) {
2439
+ let rel = relative5(fromDir, toFile);
2440
+ if (!rel.startsWith(".")) {
2441
+ rel = `./${rel}`;
2442
+ }
2443
+ return rel.replace(/\.ts$/, ".js");
2444
+ }
2445
+ async function resolveToolByName(name, config, cwd) {
2446
+ const tDir = toolsDir(config, cwd);
2447
+ const aDir = agentsDir(config, cwd);
2448
+ const lock = await readLock(cwd);
2449
+ for (const [componentName, entry] of Object.entries(lock)) {
2450
+ if (componentName === name || componentName === `${name}-tool`) {
2451
+ const toolFile = entry.files.find((f) => {
2452
+ const toolsAlias = config.aliases.tools ?? join13(config.aliases.base ?? "src/ai", "tools");
2453
+ return f.startsWith(toolsAlias);
2454
+ });
2455
+ if (toolFile) {
2456
+ const filePath2 = join13(cwd, toolFile);
2457
+ try {
2458
+ const source2 = await readFile9(filePath2, "utf-8");
2459
+ const exportName2 = parseExportName(source2);
2460
+ if (exportName2) {
2461
+ return {
2462
+ filePath: filePath2,
2463
+ exportName: exportName2,
2464
+ importPath: computeImportPath(aDir, filePath2)
2465
+ };
2466
+ }
2467
+ } catch {
2468
+ }
2469
+ }
2470
+ }
2471
+ }
2472
+ const candidates = [name, stripSuffix(name, "tool")];
2473
+ const uniqueCandidates = [...new Set(candidates)];
2474
+ const filePath = await findFile(tDir, uniqueCandidates);
2475
+ if (!filePath) return null;
2476
+ const source = await readFile9(filePath, "utf-8");
2477
+ const exportName = parseExportName(source);
2478
+ if (!exportName) return null;
2479
+ return {
2480
+ filePath,
2481
+ exportName,
2482
+ importPath: computeImportPath(aDir, filePath)
2483
+ };
2484
+ }
2485
+ async function resolveAgentByName(name, config, cwd) {
2486
+ const aDir = agentsDir(config, cwd);
2487
+ const lock = await readLock(cwd);
2488
+ for (const [componentName, entry] of Object.entries(lock)) {
2489
+ if (componentName === name || componentName === `${name}-agent`) {
2490
+ const agentFile = entry.files.find((f) => {
2491
+ const agentsAlias = config.aliases.agents ?? join13(config.aliases.base ?? "src/ai", "agents");
2492
+ return f.startsWith(agentsAlias);
2493
+ });
2494
+ if (agentFile) {
2495
+ const filePath2 = join13(cwd, agentFile);
2496
+ try {
2497
+ const source2 = await readFile9(filePath2, "utf-8");
2498
+ const agentName2 = parseAgentName(source2);
2499
+ return {
2500
+ filePath: filePath2,
2501
+ name: agentName2 ?? componentName
2502
+ };
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+ const candidates = [name, stripSuffix(name, "agent")];
2509
+ const uniqueCandidates = [...new Set(candidates)];
2510
+ const filePath = await findFile(aDir, uniqueCandidates);
2511
+ if (!filePath) return null;
2512
+ const source = await readFile9(filePath, "utf-8");
2513
+ const agentName = parseAgentName(source);
2514
+ const fallbackName = filePath.split("/").pop().replace(/\.ts$/, "");
2515
+ return {
2516
+ filePath,
2517
+ name: agentName ?? fallbackName
2518
+ };
2519
+ }
2520
+ async function listTools(config, cwd) {
2521
+ const dir = toolsDir(config, cwd);
2522
+ return listComponentsInDir(dir);
2523
+ }
2524
+ async function listAgents(config, cwd) {
2525
+ const dir = agentsDir(config, cwd);
2526
+ return listComponentsInDir(dir);
2527
+ }
2528
+ async function listComponentsInDir(dir) {
2529
+ try {
2530
+ const entries = await readdir(dir);
2531
+ return entries.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts")).sort().map((f) => ({
2532
+ name: f.replace(/\.ts$/, ""),
2533
+ filePath: join13(dir, f)
2534
+ }));
2535
+ } catch {
2536
+ return [];
2537
+ }
2538
+ }
2539
+ var init_component_resolver = __esm({
2540
+ "src/utils/component-resolver.ts"() {
2541
+ "use strict";
2542
+ init_config();
2543
+ }
2544
+ });
2545
+
2546
+ // src/installers/agent-linker.ts
2547
+ function linkToolToAgent(content, tool, toolKey) {
2548
+ const key = toolKey ?? tool.exportName;
2549
+ const { exportName, importPath } = tool;
2550
+ const toolEntry = key === exportName ? exportName : `${key}: ${exportName}`;
2551
+ if (hasToolEntry(content, key)) {
2552
+ return { content, changed: false };
2553
+ }
2554
+ const importLine = `import { ${exportName} } from "${importPath}";`;
2555
+ let result = content;
2556
+ if (!result.includes(importLine)) {
2557
+ const lines = result.split("\n");
2558
+ let lastImportIndex = -1;
2559
+ for (let i = 0; i < lines.length; i++) {
2560
+ if (/^import\s+/.test(lines[i])) lastImportIndex = i;
2561
+ }
2562
+ if (lastImportIndex === -1) {
2563
+ result = `${importLine}
2564
+ ${result}`;
2565
+ } else {
2566
+ lines.splice(lastImportIndex + 1, 0, importLine);
2567
+ result = lines.join("\n");
2568
+ }
2569
+ }
2570
+ const insertResult = insertToolEntry(result, key, exportName);
2571
+ if (insertResult.error) {
2572
+ return {
2573
+ content,
2574
+ changed: false,
2575
+ error: `Could not auto-modify the agent file. Add manually:
2576
+ 1. Import: ${importLine}
2577
+ 2. Add to tools: { ${toolEntry} }`
2578
+ };
2579
+ }
2580
+ return { content: insertResult.content, changed: true };
2581
+ }
2582
+ function unlinkToolFromAgent(content, tool, toolKey) {
2583
+ const key = toolKey ?? tool.exportName;
2584
+ const { exportName, importPath } = tool;
2585
+ if (!hasToolEntry(content, key)) {
2586
+ return { content, changed: false };
2587
+ }
2588
+ const removeResult = removeToolEntry(content, key, exportName);
2589
+ if (removeResult.error) {
2590
+ return {
2591
+ content,
2592
+ changed: false,
2593
+ error: `Could not auto-modify the agent file. Remove manually:
2594
+ 1. Remove from tools: ${key === exportName ? key : `${key}: ${exportName}`}
2595
+ 2. Remove import if unused: import { ${exportName} } from "${importPath}";`
2596
+ };
2597
+ }
2598
+ let result = removeResult.content;
2599
+ if (!isExportNameReferenced(result, exportName)) {
2600
+ result = removeImportLine(result, exportName, importPath);
2601
+ }
2602
+ return { content: result, changed: true };
2603
+ }
2604
+ function hasToolEntry(content, key) {
2605
+ const toolsMatch = extractToolsBlock(content);
2606
+ if (!toolsMatch) return false;
2607
+ const toolsContent = toolsMatch.inner;
2608
+ const keyPattern = new RegExp(
2609
+ `(?:^|[,{\\s])${escapeRegex(key)}(?:\\s*[:,}\\s]|$)`
2610
+ );
2611
+ return keyPattern.test(toolsContent);
2612
+ }
2613
+ function extractToolsBlock(content) {
2614
+ const singleLine = /^([ \t]*)tools\s*:\s*\{([^}]*)\}/m;
2615
+ const singleMatch = singleLine.exec(content);
2616
+ if (singleMatch) {
2617
+ return {
2618
+ full: singleMatch[0],
2619
+ inner: singleMatch[2],
2620
+ startIndex: singleMatch.index,
2621
+ indent: singleMatch[1]
2622
+ };
2623
+ }
2624
+ const multiStart = /^([ \t]*)tools\s*:\s*\{/m;
2625
+ const multiMatch = multiStart.exec(content);
2626
+ if (!multiMatch) return null;
2627
+ const braceStart = multiMatch.index + multiMatch[0].length;
2628
+ let depth = 1;
2629
+ let i = braceStart;
2630
+ while (i < content.length && depth > 0) {
2631
+ if (content[i] === "{") depth++;
2632
+ else if (content[i] === "}") depth--;
2633
+ i++;
2634
+ }
2635
+ if (depth !== 0) return null;
2636
+ const full = content.slice(multiMatch.index, i);
2637
+ const inner = content.slice(braceStart, i - 1);
2638
+ return {
2639
+ full,
2640
+ inner,
2641
+ startIndex: multiMatch.index,
2642
+ indent: multiMatch[1]
2643
+ };
2644
+ }
2645
+ function insertToolEntry(content, key, exportName) {
2646
+ const block = extractToolsBlock(content);
2647
+ if (!block) return { content, error: "no tools block found" };
2648
+ const entry = key === exportName ? key : `${key}: ${exportName}`;
2649
+ const trimmedInner = block.inner.trim();
2650
+ let newToolsContent;
2651
+ if (trimmedInner === "") {
2652
+ newToolsContent = `tools: { ${entry} }`;
2653
+ } else if (!block.inner.includes("\n")) {
2654
+ const cleaned = trimmedInner.replace(/,?\s*$/, "");
2655
+ newToolsContent = `tools: { ${cleaned}, ${entry} }`;
2656
+ } else {
2657
+ const entryIndent = block.indent + " ";
2658
+ const existingTrimmed = block.inner.trimEnd();
2659
+ const withComma = existingTrimmed.endsWith(",") ? existingTrimmed : existingTrimmed + ",";
2660
+ newToolsContent = `tools: {
2661
+ ${withComma}
2662
+ ${entryIndent}${entry},
2663
+ ${block.indent}}`;
2664
+ }
2665
+ const newContent = content.replace(block.full, newToolsContent);
2666
+ return { content: newContent };
2667
+ }
2668
+ function removeToolEntry(content, key, exportName) {
2669
+ const block = extractToolsBlock(content);
2670
+ if (!block) return { content, error: "no tools block found" };
2671
+ const trimmedInner = block.inner.trim();
2672
+ if (!block.inner.includes("\n")) {
2673
+ const entries = trimmedInner.split(",").map((e) => e.trim()).filter((e) => e !== "");
2674
+ const filtered = entries.filter((e) => {
2675
+ const colonIdx = e.indexOf(":");
2676
+ const entryKey = colonIdx >= 0 ? e.slice(0, colonIdx).trim() : e.trim();
2677
+ return entryKey !== key;
2678
+ });
2679
+ const newInner = filtered.length === 0 ? "" : ` ${filtered.join(", ")} `;
2680
+ const newBlock = `tools: {${newInner}}`;
2681
+ return { content: content.replace(block.full, newBlock) };
2682
+ } else {
2683
+ const lines = block.inner.split("\n");
2684
+ const keyPattern = new RegExp(
2685
+ `^\\s*${escapeRegex(key)}\\s*(?::|,|$)`
2686
+ );
2687
+ const filtered = lines.filter((line) => !keyPattern.test(line));
2688
+ const hasEntries = filtered.some((l) => l.trim() !== "");
2689
+ if (!hasEntries) {
2690
+ const newBlock2 = `tools: {}`;
2691
+ return { content: content.replace(block.full, newBlock2) };
2692
+ }
2693
+ const cleanedLines = filtered.slice();
2694
+ for (let i = cleanedLines.length - 1; i >= 0; i--) {
2695
+ if (cleanedLines[i].trim() !== "") {
2696
+ if (!cleanedLines[i].trimEnd().endsWith(",")) {
2697
+ cleanedLines[i] = cleanedLines[i].trimEnd() + ",";
2698
+ }
2699
+ break;
2700
+ }
2701
+ }
2702
+ const newBlock = `tools: {
2703
+ ${cleanedLines.join("\n")}
2704
+ ${block.indent}}`;
2705
+ return { content: content.replace(block.full, newBlock) };
2706
+ }
2707
+ }
2708
+ function isExportNameReferenced(content, exportName) {
2709
+ const lines = content.split("\n");
2710
+ for (const line of lines) {
2711
+ if (/^import\s+/.test(line)) continue;
2712
+ const wordPattern = new RegExp(`\\b${escapeRegex(exportName)}\\b`);
2713
+ if (wordPattern.test(line)) return true;
2714
+ }
2715
+ return false;
2716
+ }
2717
+ function removeImportLine(content, exportName, importPath) {
2718
+ const lines = content.split("\n");
2719
+ const result = [];
2720
+ for (let i = 0; i < lines.length; i++) {
2721
+ const line = lines[i];
2722
+ const singleImportMatch = line.match(
2723
+ /^import\s*\{([^}]+)\}\s*from\s*["'](.+?)["']\s*;?\s*$/
2724
+ );
2725
+ if (singleImportMatch && singleImportMatch[2] === importPath) {
2726
+ const imports = singleImportMatch[1].split(",").map((s) => s.trim()).filter((s) => s !== "");
2727
+ if (imports.length === 1 && imports[0] === exportName) {
2728
+ if (i + 1 < lines.length && lines[i + 1].trim() === "") {
2729
+ i++;
2730
+ }
2731
+ continue;
2732
+ }
2733
+ const remaining = imports.filter((s) => s !== exportName);
2734
+ result.push(
2735
+ `import { ${remaining.join(", ")} } from "${importPath}";`
2736
+ );
2737
+ continue;
2738
+ }
2739
+ if (/^import\s*\{/.test(line) && !line.includes("}") && content.includes(importPath)) {
2740
+ const importLines = [line];
2741
+ let j = i + 1;
2742
+ while (j < lines.length && !lines[j].includes("}")) {
2743
+ importLines.push(lines[j]);
2744
+ j++;
2745
+ }
2746
+ if (j < lines.length) {
2747
+ importLines.push(lines[j]);
2748
+ }
2749
+ const fullImport = importLines.join("\n");
2750
+ if (fullImport.includes(importPath)) {
2751
+ const namesMatch = fullImport.match(/\{([^}]+)\}/);
2752
+ if (namesMatch) {
2753
+ const imports = namesMatch[1].split(",").map((s) => s.trim()).filter((s) => s !== "");
2754
+ if (imports.length === 1 && imports[0] === exportName) {
2755
+ i = j;
2756
+ if (i + 1 < lines.length && lines[i + 1].trim() === "") {
2757
+ i++;
2758
+ }
2759
+ continue;
2760
+ }
2761
+ const remaining = imports.filter((s) => s !== exportName);
2762
+ if (remaining.length <= 2) {
2763
+ result.push(
2764
+ `import { ${remaining.join(", ")} } from "${importPath}";`
2765
+ );
2766
+ } else {
2767
+ result.push(`import {`);
2768
+ remaining.forEach((name, idx) => {
2769
+ result.push(
2770
+ ` ${name}${idx < remaining.length - 1 ? "," : ""}`
2771
+ );
2772
+ });
2773
+ result.push(`} from "${importPath}";`);
2774
+ }
2775
+ i = j;
2776
+ continue;
2777
+ }
2778
+ }
2779
+ }
2780
+ result.push(line);
2781
+ }
2782
+ return result.join("\n");
2783
+ }
2784
+ function escapeRegex(s) {
2785
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2786
+ }
2787
+ var init_agent_linker = __esm({
2788
+ "src/installers/agent-linker.ts"() {
2789
+ "use strict";
2790
+ }
2791
+ });
2792
+
2793
+ // src/commands/link.ts
2794
+ var link_exports = {};
2795
+ __export(link_exports, {
2796
+ linkCommand: () => linkCommand
2797
+ });
2798
+ import * as p9 from "@clack/prompts";
2799
+ import pc8 from "picocolors";
2800
+ import { readFile as readFile10, writeFile as writeFile11 } from "fs/promises";
2801
+ import { basename } from "path";
2802
+ async function linkCommand(type, name, opts) {
2803
+ p9.intro(pc8.bgCyan(pc8.black(" kitn link ")));
2804
+ const cwd = process.cwd();
2805
+ const config = await readConfig(cwd);
2806
+ if (!config) {
2807
+ p9.log.error("No kitn.json found. Run `kitn init` first.");
2808
+ process.exit(1);
2809
+ }
2810
+ if (type && type !== "tool") {
2811
+ p9.log.error(
2812
+ `Unsupported type "${type}". Only ${pc8.bold("tool")} is supported.`
2813
+ );
2814
+ process.exit(1);
2815
+ }
2816
+ let toolName = name;
2817
+ if (!toolName) {
2818
+ const tools = await listTools(config, cwd);
2819
+ if (tools.length === 0) {
2820
+ p9.log.error("No tools found in your project.");
2821
+ process.exit(1);
2822
+ }
2823
+ const selected = await p9.select({
2824
+ message: "Select a tool to link:",
2825
+ options: tools.map((t) => ({
2826
+ value: t.name,
2827
+ label: t.name
2828
+ }))
2829
+ });
2830
+ if (p9.isCancel(selected)) {
2831
+ p9.cancel("Cancelled.");
2832
+ process.exit(0);
2833
+ }
2834
+ toolName = selected;
2835
+ }
2836
+ const tool = await resolveToolByName(toolName, config, cwd);
2837
+ if (!tool) {
2838
+ p9.log.error(
2839
+ `Tool "${toolName}" not found. Check that the file exists in your tools directory.`
2840
+ );
2841
+ process.exit(1);
2842
+ }
2843
+ let agentName = opts?.to;
2844
+ if (!agentName) {
2845
+ const agents = await listAgents(config, cwd);
2846
+ if (agents.length === 0) {
2847
+ p9.log.error("No agents found in your project.");
2848
+ process.exit(1);
2849
+ }
2850
+ const selected = await p9.select({
2851
+ message: "Select an agent to link the tool to:",
2852
+ options: agents.map((a) => ({
2853
+ value: a.name,
2854
+ label: a.name
2855
+ }))
2856
+ });
2857
+ if (p9.isCancel(selected)) {
2858
+ p9.cancel("Cancelled.");
2859
+ process.exit(0);
2860
+ }
2861
+ agentName = selected;
2862
+ }
2863
+ const agent = await resolveAgentByName(agentName, config, cwd);
2864
+ if (!agent) {
2865
+ p9.log.error(
2866
+ `Agent "${agentName}" not found. Check that the file exists in your agents directory.`
2867
+ );
2868
+ process.exit(1);
2869
+ }
2870
+ const agentContent = await readFile10(agent.filePath, "utf-8");
2871
+ const toolRef = {
2872
+ exportName: tool.exportName,
2873
+ importPath: tool.importPath
2874
+ };
2875
+ const result = linkToolToAgent(agentContent, toolRef, opts?.as);
2876
+ if (result.error) {
2877
+ p9.log.warn(result.error);
2878
+ p9.outro("Could not auto-link. Follow the manual instructions above.");
2879
+ process.exit(1);
2880
+ }
2881
+ if (!result.changed) {
2882
+ p9.log.info(
2883
+ `${pc8.cyan(tool.exportName)} is already linked to ${pc8.cyan(basename(agent.filePath))}.`
2884
+ );
2885
+ p9.outro("Nothing to do.");
2886
+ return;
2887
+ }
2888
+ await writeFile11(agent.filePath, result.content);
2889
+ p9.log.success(
2890
+ `Linked ${pc8.cyan(tool.exportName)} to ${pc8.cyan(basename(agent.filePath))}`
2891
+ );
2892
+ p9.log.message(
2893
+ ` ${pc8.green("+")} import { ${tool.exportName} } from "${tool.importPath}"`
2894
+ );
2895
+ p9.log.message(
2896
+ ` ${pc8.green("+")} tools: { ${opts?.as ? `${opts.as}: ${tool.exportName}` : tool.exportName} }`
2897
+ );
2898
+ p9.outro("Done!");
2899
+ }
2900
+ var init_link = __esm({
2901
+ "src/commands/link.ts"() {
2902
+ "use strict";
2903
+ init_config();
2904
+ init_component_resolver();
2905
+ init_agent_linker();
2906
+ }
2907
+ });
2908
+
2909
+ // src/commands/unlink.ts
2910
+ var unlink_exports = {};
2911
+ __export(unlink_exports, {
2912
+ unlinkCommand: () => unlinkCommand
2913
+ });
2914
+ import * as p10 from "@clack/prompts";
2915
+ import pc9 from "picocolors";
2916
+ import { readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
2917
+ import { basename as basename2 } from "path";
2918
+ async function unlinkCommand(type, name, opts) {
2919
+ p10.intro(pc9.bgCyan(pc9.black(" kitn unlink ")));
2920
+ const cwd = process.cwd();
2921
+ const config = await readConfig(cwd);
2922
+ if (!config) {
2923
+ p10.log.error("No kitn.json found. Run `kitn init` first.");
2924
+ process.exit(1);
2925
+ }
2926
+ if (type && type !== "tool") {
2927
+ p10.log.error(
2928
+ `Unsupported type "${type}". Only ${pc9.bold("tool")} is supported.`
2929
+ );
2930
+ process.exit(1);
2931
+ }
2932
+ let toolName = name;
2933
+ if (!toolName) {
2934
+ const tools = await listTools(config, cwd);
2935
+ if (tools.length === 0) {
2936
+ p10.log.error("No tools found in your project.");
2937
+ process.exit(1);
2938
+ }
2939
+ const selected = await p10.select({
2940
+ message: "Select a tool to unlink:",
2941
+ options: tools.map((t) => ({
2942
+ value: t.name,
2943
+ label: t.name
2944
+ }))
2945
+ });
2946
+ if (p10.isCancel(selected)) {
2947
+ p10.cancel("Cancelled.");
2948
+ process.exit(0);
2949
+ }
2950
+ toolName = selected;
2951
+ }
2952
+ const tool = await resolveToolByName(toolName, config, cwd);
2953
+ if (!tool) {
2954
+ p10.log.error(
2955
+ `Tool "${toolName}" not found. Check that the file exists in your tools directory.`
2956
+ );
2957
+ process.exit(1);
2958
+ }
2959
+ let agentName = opts?.from;
2960
+ if (!agentName) {
2961
+ const agents = await listAgents(config, cwd);
2962
+ if (agents.length === 0) {
2963
+ p10.log.error("No agents found in your project.");
2964
+ process.exit(1);
2965
+ }
2966
+ const selected = await p10.select({
2967
+ message: "Select an agent to unlink the tool from:",
2968
+ options: agents.map((a) => ({
2969
+ value: a.name,
2970
+ label: a.name
2971
+ }))
2972
+ });
2973
+ if (p10.isCancel(selected)) {
2974
+ p10.cancel("Cancelled.");
2975
+ process.exit(0);
2976
+ }
2977
+ agentName = selected;
2978
+ }
2979
+ const agent = await resolveAgentByName(agentName, config, cwd);
2980
+ if (!agent) {
2981
+ p10.log.error(
2982
+ `Agent "${agentName}" not found. Check that the file exists in your agents directory.`
2983
+ );
2984
+ process.exit(1);
2985
+ }
2986
+ const agentContent = await readFile11(agent.filePath, "utf-8");
2987
+ const toolRef = {
2988
+ exportName: tool.exportName,
2989
+ importPath: tool.importPath
2990
+ };
2991
+ const result = unlinkToolFromAgent(agentContent, toolRef);
2992
+ if (result.error) {
2993
+ p10.log.warn(result.error);
2994
+ p10.outro("Could not auto-unlink. Follow the manual instructions above.");
2995
+ process.exit(1);
2996
+ }
2997
+ if (!result.changed) {
2998
+ p10.log.info(
2999
+ `${pc9.cyan(tool.exportName)} is not linked to ${pc9.cyan(basename2(agent.filePath))}.`
3000
+ );
3001
+ p10.outro("Nothing to do.");
3002
+ return;
3003
+ }
3004
+ await writeFile12(agent.filePath, result.content);
3005
+ p10.log.success(
3006
+ `Unlinked ${pc9.cyan(tool.exportName)} from ${pc9.cyan(basename2(agent.filePath))}`
3007
+ );
3008
+ p10.log.message(
3009
+ ` ${pc9.red("-")} import { ${tool.exportName} } from "${tool.importPath}"`
3010
+ );
3011
+ p10.log.message(
3012
+ ` ${pc9.red("-")} tools: { ${tool.exportName} }`
3013
+ );
3014
+ p10.outro("Done!");
3015
+ }
3016
+ var init_unlink = __esm({
3017
+ "src/commands/unlink.ts"() {
3018
+ "use strict";
3019
+ init_config();
3020
+ init_component_resolver();
3021
+ init_agent_linker();
3022
+ }
3023
+ });
3024
+
2219
3025
  // src/commands/info.ts
2220
3026
  var info_exports = {};
2221
3027
  __export(info_exports, {
2222
3028
  infoCommand: () => infoCommand
2223
3029
  });
2224
- import * as p9 from "@clack/prompts";
2225
- import pc8 from "picocolors";
3030
+ import * as p11 from "@clack/prompts";
3031
+ import pc10 from "picocolors";
2226
3032
  async function infoCommand(component) {
2227
3033
  const cwd = process.cwd();
2228
3034
  const config = await readConfig(cwd);
2229
3035
  if (!config) {
2230
- p9.log.error("No kitn.json found. Run `kitn init` first.");
3036
+ p11.log.error("No kitn.json found. Run `kitn init` first.");
2231
3037
  process.exit(1);
2232
3038
  }
2233
3039
  const ref = parseComponentRef(component);
2234
3040
  const fetcher = new RegistryFetcher(config.registries);
2235
- const s = p9.spinner();
3041
+ const s = p11.spinner();
2236
3042
  s.start("Fetching component info...");
2237
3043
  let index;
2238
3044
  try {
2239
3045
  index = await fetcher.fetchIndex(ref.namespace);
2240
3046
  } catch (err) {
2241
- s.stop(pc8.red("Failed to fetch registry"));
2242
- p9.log.error(err.message);
3047
+ s.stop(pc10.red("Failed to fetch registry"));
3048
+ p11.log.error(err.message);
2243
3049
  process.exit(1);
2244
3050
  }
2245
3051
  const indexItem = index.items.find((i) => i.name === ref.name);
2246
3052
  if (!indexItem) {
2247
- s.stop(pc8.red("Component not found"));
2248
- p9.log.error(`Component '${ref.name}' not found in registry.`);
3053
+ s.stop(pc10.red("Component not found"));
3054
+ p11.log.error(`Component '${ref.name}' not found in registry.`);
2249
3055
  process.exit(1);
2250
3056
  }
2251
3057
  const dir = typeToDir[indexItem.type];
@@ -2253,8 +3059,8 @@ async function infoCommand(component) {
2253
3059
  try {
2254
3060
  item = await fetcher.fetchItem(ref.name, dir, ref.namespace, ref.version);
2255
3061
  } catch (err) {
2256
- s.stop(pc8.red("Failed to fetch component"));
2257
- p9.log.error(err.message);
3062
+ s.stop(pc10.red("Failed to fetch component"));
3063
+ p11.log.error(err.message);
2258
3064
  process.exit(1);
2259
3065
  }
2260
3066
  s.stop("Component found");
@@ -2262,63 +3068,63 @@ async function infoCommand(component) {
2262
3068
  const typeName = indexItem.type.replace("kitn:", "");
2263
3069
  console.log();
2264
3070
  console.log(
2265
- ` ${pc8.bold(item.name)} ${pc8.cyan(`v${version}`)}${" ".repeat(Math.max(1, 40 - item.name.length - version.length - 2))}${pc8.dim(ref.namespace)}`
3071
+ ` ${pc10.bold(item.name)} ${pc10.cyan(`v${version}`)}${" ".repeat(Math.max(1, 40 - item.name.length - version.length - 2))}${pc10.dim(ref.namespace)}`
2266
3072
  );
2267
- console.log(` ${pc8.dim(item.description)}`);
3073
+ console.log(` ${pc10.dim(item.description)}`);
2268
3074
  console.log();
2269
- console.log(` ${pc8.dim("Type:")} ${typeName}`);
3075
+ console.log(` ${pc10.dim("Type:")} ${typeName}`);
2270
3076
  if (item.dependencies?.length) {
2271
3077
  console.log(
2272
- ` ${pc8.dim("Dependencies:")} ${item.dependencies.join(", ")}`
3078
+ ` ${pc10.dim("Dependencies:")} ${item.dependencies.join(", ")}`
2273
3079
  );
2274
3080
  }
2275
3081
  if (item.registryDependencies?.length) {
2276
3082
  console.log(
2277
- ` ${pc8.dim("Registry deps:")} ${item.registryDependencies.join(", ")}`
3083
+ ` ${pc10.dim("Registry deps:")} ${item.registryDependencies.join(", ")}`
2278
3084
  );
2279
3085
  }
2280
3086
  if (item.categories?.length) {
2281
3087
  console.log(
2282
- ` ${pc8.dim("Categories:")} ${item.categories.join(", ")}`
3088
+ ` ${pc10.dim("Categories:")} ${item.categories.join(", ")}`
2283
3089
  );
2284
3090
  }
2285
3091
  if (item.updatedAt) {
2286
- console.log(` ${pc8.dim("Updated:")} ${item.updatedAt}`);
3092
+ console.log(` ${pc10.dim("Updated:")} ${item.updatedAt}`);
2287
3093
  }
2288
3094
  const versions = indexItem.versions;
2289
3095
  if (versions?.length) {
2290
- console.log(` ${pc8.dim("Versions:")} ${versions.join(", ")}`);
3096
+ console.log(` ${pc10.dim("Versions:")} ${versions.join(", ")}`);
2291
3097
  }
2292
3098
  if (item.changelog?.length) {
2293
3099
  console.log();
2294
- console.log(` ${pc8.bold("Changelog:")}`);
3100
+ console.log(` ${pc10.bold("Changelog:")}`);
2295
3101
  for (const entry of item.changelog) {
2296
- const tag = entry.type === "feature" ? pc8.green(entry.type) : entry.type === "fix" ? pc8.yellow(entry.type) : entry.type === "breaking" ? pc8.red(entry.type) : pc8.dim(entry.type);
3102
+ const tag = entry.type === "feature" ? pc10.green(entry.type) : entry.type === "fix" ? pc10.yellow(entry.type) : entry.type === "breaking" ? pc10.red(entry.type) : pc10.dim(entry.type);
2297
3103
  console.log(
2298
- ` ${pc8.cyan(entry.version)} ${pc8.dim(entry.date)} ${tag} ${entry.note}`
3104
+ ` ${pc10.cyan(entry.version)} ${pc10.dim(entry.date)} ${tag} ${entry.note}`
2299
3105
  );
2300
3106
  }
2301
3107
  }
2302
3108
  console.log();
2303
3109
  const fileCount = item.files.length;
2304
- console.log(` ${pc8.bold(`Files:`)} ${pc8.dim(`(${fileCount})`)}`);
3110
+ console.log(` ${pc10.bold(`Files:`)} ${pc10.dim(`(${fileCount})`)}`);
2305
3111
  const maxShown = 10;
2306
3112
  for (const file of item.files.slice(0, maxShown)) {
2307
- console.log(` ${pc8.dim(file.path)}`);
3113
+ console.log(` ${pc10.dim(file.path)}`);
2308
3114
  }
2309
3115
  if (fileCount > maxShown) {
2310
- console.log(` ${pc8.dim(`... and ${fileCount - maxShown} more`)}`);
3116
+ console.log(` ${pc10.dim(`... and ${fileCount - maxShown} more`)}`);
2311
3117
  }
2312
3118
  const lock = await readLock(cwd);
2313
3119
  const installed = lock[item.name];
2314
3120
  if (installed) {
2315
3121
  console.log();
2316
3122
  console.log(
2317
- ` ${pc8.green("Installed")} ${pc8.dim(`v${installed.version}`)}`
3123
+ ` ${pc10.green("Installed")} ${pc10.dim(`v${installed.version}`)}`
2318
3124
  );
2319
3125
  if (version !== installed.version) {
2320
3126
  console.log(
2321
- ` ${pc8.yellow("Update available:")} ${pc8.dim(`v${installed.version}`)} \u2192 ${pc8.cyan(`v${version}`)}`
3127
+ ` ${pc10.yellow("Update available:")} ${pc10.dim(`v${installed.version}`)} \u2192 ${pc10.cyan(`v${version}`)}`
2322
3128
  );
2323
3129
  }
2324
3130
  }
@@ -2340,41 +3146,41 @@ __export(check_exports, {
2340
3146
  checkCommand: () => checkCommand
2341
3147
  });
2342
3148
  import { execSync as execSync2 } from "child_process";
2343
- import * as p10 from "@clack/prompts";
2344
- import pc9 from "picocolors";
3149
+ import * as p12 from "@clack/prompts";
3150
+ import pc11 from "picocolors";
2345
3151
  async function checkCommand(currentVersion) {
2346
- p10.intro(pc9.bgCyan(pc9.black(" kitn check ")));
2347
- p10.log.info(`kitn v${currentVersion}`);
2348
- const s = p10.spinner();
3152
+ p12.intro(pc11.bgCyan(pc11.black(" kitn check ")));
3153
+ p12.log.info(`kitn v${currentVersion}`);
3154
+ const s = p12.spinner();
2349
3155
  s.start("Checking for updates...");
2350
3156
  const latest = await fetchLatestVersion();
2351
3157
  if (!latest) {
2352
- s.stop(pc9.yellow("Could not reach the npm registry"));
2353
- p10.outro("Try again later.");
3158
+ s.stop(pc11.yellow("Could not reach the npm registry"));
3159
+ p12.outro("Try again later.");
2354
3160
  return;
2355
3161
  }
2356
3162
  if (isNewer(latest, currentVersion)) {
2357
- s.stop(pc9.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
3163
+ s.stop(pc11.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
2358
3164
  const pm = detectCliInstaller();
2359
3165
  const installCmd = getGlobalInstallCommand(pm, "@kitnai/cli");
2360
- const shouldUpdate = await p10.confirm({ message: "Update now?" });
2361
- if (p10.isCancel(shouldUpdate) || !shouldUpdate) {
2362
- p10.log.message(` Run: ${pc9.cyan(installCmd)}`);
3166
+ const shouldUpdate = await p12.confirm({ message: "Update now?" });
3167
+ if (p12.isCancel(shouldUpdate) || !shouldUpdate) {
3168
+ p12.log.message(` Run: ${pc11.cyan(installCmd)}`);
2363
3169
  } else {
2364
- const us = p10.spinner();
3170
+ const us = p12.spinner();
2365
3171
  us.start("Updating...");
2366
3172
  try {
2367
3173
  execSync2(installCmd, { stdio: "pipe" });
2368
- us.stop(pc9.green(`Updated to v${latest}`));
3174
+ us.stop(pc11.green(`Updated to v${latest}`));
2369
3175
  } catch {
2370
- us.stop(pc9.red("Update failed"));
2371
- p10.log.message(` Run manually: ${pc9.cyan(installCmd)}`);
3176
+ us.stop(pc11.red("Update failed"));
3177
+ p12.log.message(` Run manually: ${pc11.cyan(installCmd)}`);
2372
3178
  }
2373
3179
  }
2374
3180
  } else {
2375
- s.stop(pc9.green("You're on the latest version"));
3181
+ s.stop(pc11.green("You're on the latest version"));
2376
3182
  }
2377
- p10.outro("");
3183
+ p12.outro("");
2378
3184
  }
2379
3185
  var init_check = __esm({
2380
3186
  "src/commands/check.ts"() {
@@ -2384,6 +3190,64 @@ var init_check = __esm({
2384
3190
  }
2385
3191
  });
2386
3192
 
3193
+ // src/commands/rules.ts
3194
+ var rules_exports = {};
3195
+ __export(rules_exports, {
3196
+ rulesCommand: () => rulesCommand
3197
+ });
3198
+ import * as p13 from "@clack/prompts";
3199
+ import pc12 from "picocolors";
3200
+ async function rulesCommand() {
3201
+ p13.intro(pc12.bgCyan(pc12.black(" kitn rules ")));
3202
+ const cwd = process.cwd();
3203
+ const config = await readConfig(cwd);
3204
+ if (!config) {
3205
+ p13.log.error(`No kitn.json found. Run ${pc12.bold("kitn init")} first.`);
3206
+ process.exit(1);
3207
+ }
3208
+ let selectedIds = config.aiTools;
3209
+ if (!selectedIds || selectedIds.length === 0) {
3210
+ const rulesConfig = await fetchRulesConfig(config.registries);
3211
+ const selected = await p13.multiselect({
3212
+ message: "Which AI coding tools do you use?",
3213
+ options: rulesConfig.tools.map((t) => ({
3214
+ value: t.id,
3215
+ label: t.name,
3216
+ hint: t.description
3217
+ })),
3218
+ required: false
3219
+ });
3220
+ if (p13.isCancel(selected)) {
3221
+ p13.cancel("Cancelled.");
3222
+ process.exit(0);
3223
+ }
3224
+ selectedIds = selected;
3225
+ if (selectedIds.length === 0) {
3226
+ p13.log.warn("No tools selected. Nothing to generate.");
3227
+ p13.outro("Done.");
3228
+ return;
3229
+ }
3230
+ const updatedConfig = { ...config, aiTools: selectedIds };
3231
+ await writeConfig(cwd, updatedConfig);
3232
+ p13.log.info(`Saved tool selections to ${pc12.bold("kitn.json")}`);
3233
+ }
3234
+ const s = p13.spinner();
3235
+ s.start("Generating rules files");
3236
+ const written = await generateRulesFiles(cwd, config, selectedIds);
3237
+ s.stop("Rules files generated");
3238
+ for (const filePath of written) {
3239
+ p13.log.success(`${pc12.green("+")} ${filePath}`);
3240
+ }
3241
+ p13.outro(`Generated ${written.length} rules file${written.length === 1 ? "" : "s"}.`);
3242
+ }
3243
+ var init_rules = __esm({
3244
+ "src/commands/rules.ts"() {
3245
+ "use strict";
3246
+ init_config();
3247
+ init_rules_generator();
3248
+ }
3249
+ });
3250
+
2387
3251
  // src/commands/registry.ts
2388
3252
  var registry_exports = {};
2389
3253
  __export(registry_exports, {
@@ -2391,8 +3255,8 @@ __export(registry_exports, {
2391
3255
  registryListCommand: () => registryListCommand,
2392
3256
  registryRemoveCommand: () => registryRemoveCommand
2393
3257
  });
2394
- import * as p11 from "@clack/prompts";
2395
- import pc10 from "picocolors";
3258
+ import * as p14 from "@clack/prompts";
3259
+ import pc13 from "picocolors";
2396
3260
  async function registryAddCommand(namespace, url, opts = {}) {
2397
3261
  const cwd = opts.cwd ?? process.cwd();
2398
3262
  const config = await readConfig(cwd);
@@ -2418,10 +3282,10 @@ async function registryAddCommand(namespace, url, opts = {}) {
2418
3282
  config.registries[namespace] = url;
2419
3283
  }
2420
3284
  await writeConfig(cwd, config);
2421
- p11.log.success(`Added registry ${pc10.bold(namespace)}`);
2422
- p11.log.message(pc10.dim(` ${url}`));
2423
- if (opts.homepage) p11.log.message(pc10.dim(` Homepage: ${opts.homepage}`));
2424
- if (opts.description) p11.log.message(pc10.dim(` ${opts.description}`));
3285
+ p14.log.success(`Added registry ${pc13.bold(namespace)}`);
3286
+ p14.log.message(pc13.dim(` ${url}`));
3287
+ if (opts.homepage) p14.log.message(pc13.dim(` Homepage: ${opts.homepage}`));
3288
+ if (opts.description) p14.log.message(pc13.dim(` ${opts.description}`));
2425
3289
  }
2426
3290
  async function registryRemoveCommand(namespace, opts = {}) {
2427
3291
  const cwd = opts.cwd ?? process.cwd();
@@ -2442,10 +3306,10 @@ async function registryRemoveCommand(namespace, opts = {}) {
2442
3306
  }
2443
3307
  delete config.registries[namespace];
2444
3308
  await writeConfig(cwd, config);
2445
- p11.log.success(`Removed registry ${pc10.bold(namespace)}`);
3309
+ p14.log.success(`Removed registry ${pc13.bold(namespace)}`);
2446
3310
  if (affectedComponents.length > 0) {
2447
- p11.log.warn(`${affectedComponents.length} installed component(s) referenced this registry:
2448
- ` + affectedComponents.map((name) => ` ${pc10.yellow("!")} ${name}`).join("\n"));
3311
+ p14.log.warn(`${affectedComponents.length} installed component(s) referenced this registry:
3312
+ ` + affectedComponents.map((name) => ` ${pc13.yellow("!")} ${name}`).join("\n"));
2449
3313
  }
2450
3314
  return { affectedComponents };
2451
3315
  }
@@ -2460,15 +3324,15 @@ async function registryListCommand(opts = {}) {
2460
3324
  return { namespace, url, homepage, description };
2461
3325
  });
2462
3326
  if (entries.length === 0) {
2463
- p11.log.message(pc10.dim(" No registries configured."));
3327
+ p14.log.message(pc13.dim(" No registries configured."));
2464
3328
  } else {
2465
3329
  const lines = [];
2466
3330
  for (const { namespace, url, homepage, description } of entries) {
2467
- lines.push(` ${pc10.bold(namespace.padEnd(16))} ${pc10.dim(url)}`);
3331
+ lines.push(` ${pc13.bold(namespace.padEnd(16))} ${pc13.dim(url)}`);
2468
3332
  if (description) lines.push(` ${" ".repeat(16)} ${description}`);
2469
- if (homepage) lines.push(` ${" ".repeat(16)} ${pc10.dim(homepage)}`);
3333
+ if (homepage) lines.push(` ${" ".repeat(16)} ${pc13.dim(homepage)}`);
2470
3334
  }
2471
- p11.log.message(lines.join("\n"));
3335
+ p14.log.message(lines.join("\n"));
2472
3336
  }
2473
3337
  return entries;
2474
3338
  }
@@ -2482,7 +3346,7 @@ var init_registry = __esm({
2482
3346
  // src/index.ts
2483
3347
  init_update_check();
2484
3348
  import { Command } from "commander";
2485
- var VERSION = true ? "0.1.30" : "0.0.0-dev";
3349
+ var VERSION = true ? "0.1.31" : "0.0.0-dev";
2486
3350
  var printUpdateNotice = startUpdateCheck(VERSION);
2487
3351
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
2488
3352
  program.command("init").description("Initialize kitn in your project").option("-r, --runtime <runtime>", "runtime to use (bun, node, deno)").option("-f, --framework <framework>", "HTTP framework (hono, hono-openapi, elysia)").option("-b, --base <path>", "base directory for components (default: src/ai)").option("-y, --yes", "accept all defaults without prompting").action(async (opts) => {
@@ -2513,6 +3377,14 @@ program.command("create").description("Scaffold a new kitn component").argument(
2513
3377
  const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
2514
3378
  await createCommand2(type, name);
2515
3379
  });
3380
+ program.command("link").description("Wire a tool to an agent").argument("[type]", "component type (tool)").argument("[name]", "component name").option("--to <agent>", "target agent name").option("--as <key>", "key name in the tools object").action(async (type, name, opts) => {
3381
+ const { linkCommand: linkCommand2 } = await Promise.resolve().then(() => (init_link(), link_exports));
3382
+ await linkCommand2(type, name, opts);
3383
+ });
3384
+ program.command("unlink").description("Unwire a tool from an agent").argument("[type]", "component type (tool)").argument("[name]", "component name").option("--from <agent>", "target agent name").action(async (type, name, opts) => {
3385
+ const { unlinkCommand: unlinkCommand2 } = await Promise.resolve().then(() => (init_unlink(), unlink_exports));
3386
+ await unlinkCommand2(type, name, opts);
3387
+ });
2516
3388
  program.command("info").description("Show details about a component").argument("<component>", "component name (e.g. weather-agent, @acme/tool@1.0.0)").action(async (component) => {
2517
3389
  const { infoCommand: infoCommand2 } = await Promise.resolve().then(() => (init_info(), info_exports));
2518
3390
  await infoCommand2(component);
@@ -2521,6 +3393,10 @@ program.command("check").description("Check for CLI updates").action(async () =>
2521
3393
  const { checkCommand: checkCommand2 } = await Promise.resolve().then(() => (init_check(), check_exports));
2522
3394
  await checkCommand2(VERSION);
2523
3395
  });
3396
+ program.command("rules").description("Regenerate AI coding tool rules files").action(async () => {
3397
+ const { rulesCommand: rulesCommand2 } = await Promise.resolve().then(() => (init_rules(), rules_exports));
3398
+ await rulesCommand2();
3399
+ });
2524
3400
  var registry = program.command("registry").description("Manage component registries");
2525
3401
  registry.command("add").description("Add a component registry").argument("<namespace>", "registry namespace (e.g. @myteam)").argument("<url>", "URL template with {type} and {name} placeholders").option("-o, --overwrite", "overwrite if namespace already exists").option("--homepage <url>", "registry homepage URL").option("--description <text>", "short description of the registry").action(async (namespace, url, opts) => {
2526
3402
  const { registryAddCommand: registryAddCommand2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));