@kitnai/cli 0.1.30 → 0.1.32

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,20 @@ __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, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1572
+ import { join as join9 } from "path";
1573
+ async function detectFramework(cwd) {
1574
+ try {
1575
+ const pkg = JSON.parse(await readFile7(join9(cwd, "package.json"), "utf-8"));
1576
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1577
+ if (deps["elysia"]) return "elysia";
1578
+ if (deps["@hono/zod-openapi"]) return "hono-openapi";
1579
+ if (deps["hono"]) return "hono";
1580
+ return null;
1581
+ } catch {
1582
+ return null;
1583
+ }
1584
+ }
1433
1585
  function getPluginTemplate(framework) {
1434
1586
  const adapterName = framework === "hono-openapi" ? "hono-openapi" : framework;
1435
1587
  return `import { createAIPlugin } from "@kitn/adapters/${adapterName}";
@@ -1497,12 +1649,41 @@ async function initCommand(opts = {}) {
1497
1649
  }
1498
1650
  const validFrameworks = ["hono", "hono-openapi", "elysia"];
1499
1651
  let framework;
1652
+ const detected = await detectFramework(cwd);
1500
1653
  if (opts.framework) {
1501
1654
  if (!validFrameworks.includes(opts.framework)) {
1502
1655
  p3.log.error(`Invalid framework: ${opts.framework}. Must be one of: ${validFrameworks.join(", ")}`);
1503
1656
  process.exit(1);
1504
1657
  }
1505
1658
  framework = opts.framework;
1659
+ } else if (detected) {
1660
+ framework = detected;
1661
+ p3.log.info(`Detected ${pc4.bold(detected)} from package.json`);
1662
+ if (!opts.yes) {
1663
+ const confirm6 = await p3.confirm({
1664
+ message: `Use ${detected}?`,
1665
+ initialValue: true
1666
+ });
1667
+ if (p3.isCancel(confirm6)) {
1668
+ p3.cancel("Init cancelled.");
1669
+ process.exit(0);
1670
+ }
1671
+ if (!confirm6) {
1672
+ const selected = await p3.select({
1673
+ message: "Which HTTP framework do you use?",
1674
+ options: [
1675
+ { value: "hono", label: "Hono" },
1676
+ { value: "hono-openapi", label: "Hono + OpenAPI", hint: "zod-openapi routes with /doc endpoint" },
1677
+ { value: "elysia", label: "Elysia", hint: "Bun-native framework" }
1678
+ ]
1679
+ });
1680
+ if (p3.isCancel(selected)) {
1681
+ p3.cancel("Init cancelled.");
1682
+ process.exit(0);
1683
+ }
1684
+ framework = selected;
1685
+ }
1686
+ }
1506
1687
  } else if (opts.yes) {
1507
1688
  framework = "hono";
1508
1689
  } else {
@@ -1567,13 +1748,45 @@ async function initCommand(opts = {}) {
1567
1748
  p3.log.info(`Patched tsconfig.json with path: ${pc4.bold("@kitn/*")}`);
1568
1749
  p3.log.info("Installing core engine and adapter...");
1569
1750
  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));
1751
+ const aiDir = join9(cwd, baseDir);
1752
+ await mkdir5(aiDir, { recursive: true });
1753
+ const barrelPath = join9(aiDir, "index.ts");
1754
+ await writeFile8(barrelPath, createBarrelFile());
1755
+ const pluginPath = join9(aiDir, "plugin.ts");
1756
+ await writeFile8(pluginPath, getPluginTemplate(framework));
1576
1757
  p3.log.success(`Created ${pc4.bold(baseDir + "/plugin.ts")} \u2014 configure your AI provider there`);
1758
+ try {
1759
+ const rulesConfig = await fetchRulesConfig(config.registries);
1760
+ let selectedToolIds;
1761
+ if (opts.yes) {
1762
+ selectedToolIds = rulesConfig.tools.map((t) => t.id);
1763
+ } else {
1764
+ const selected = await p3.multiselect({
1765
+ message: "Which AI coding tools do you use?",
1766
+ options: rulesConfig.tools.map((t) => ({
1767
+ value: t.id,
1768
+ label: t.name,
1769
+ hint: t.description
1770
+ })),
1771
+ required: false
1772
+ });
1773
+ if (p3.isCancel(selected)) {
1774
+ selectedToolIds = [];
1775
+ } else {
1776
+ selectedToolIds = selected;
1777
+ }
1778
+ }
1779
+ if (selectedToolIds.length > 0) {
1780
+ const updatedConfig = { ...config, aiTools: selectedToolIds };
1781
+ await writeConfig(cwd, updatedConfig);
1782
+ const written = await generateRulesFiles(cwd, updatedConfig, selectedToolIds);
1783
+ for (const filePath of written) {
1784
+ p3.log.success(`Created ${pc4.bold(filePath)}`);
1785
+ }
1786
+ }
1787
+ } catch {
1788
+ p3.log.warn("Could not generate AI coding tool rules (non-fatal).");
1789
+ }
1577
1790
  const mountCode = framework === "elysia" ? `app.use(new Elysia({ prefix: "/api" }).use(ai.router));` : `app.route("/api", ai.router);`;
1578
1791
  p3.note(
1579
1792
  [
@@ -1601,6 +1814,7 @@ var init_init = __esm({
1601
1814
  init_tsconfig_patcher();
1602
1815
  init_barrel_manager();
1603
1816
  init_add();
1817
+ init_rules_generator();
1604
1818
  }
1605
1819
  });
1606
1820
 
@@ -1736,7 +1950,7 @@ __export(diff_exports, {
1736
1950
  diffCommand: () => diffCommand
1737
1951
  });
1738
1952
  import * as p5 from "@clack/prompts";
1739
- import { join as join9 } from "path";
1953
+ import { join as join10 } from "path";
1740
1954
  async function diffCommand(componentName) {
1741
1955
  const cwd = process.cwd();
1742
1956
  const config = await readConfig(cwd);
@@ -1767,8 +1981,8 @@ async function diffCommand(componentName) {
1767
1981
  for (const file of registryItem.files) {
1768
1982
  if (indexItem.type === "kitn:package") {
1769
1983
  const baseDir = config.aliases.base ?? "src/ai";
1770
- const localPath = join9(cwd, baseDir, file.path);
1771
- const relativePath = join9(baseDir, file.path);
1984
+ const localPath = join10(cwd, baseDir, file.path);
1985
+ const relativePath = join10(baseDir, file.path);
1772
1986
  const localContent = await readExistingFile(localPath);
1773
1987
  if (localContent === null) {
1774
1988
  p5.log.warn(`${relativePath}: file missing locally`);
@@ -1792,7 +2006,7 @@ async function diffCommand(componentName) {
1792
2006
  return "storage";
1793
2007
  }
1794
2008
  })();
1795
- const localPath = join9(cwd, config.aliases[aliasKey], fileName);
2009
+ const localPath = join10(cwd, config.aliases[aliasKey], fileName);
1796
2010
  const localContent = await readExistingFile(localPath);
1797
2011
  if (localContent === null) {
1798
2012
  p5.log.warn(`${fileName}: file missing locally`);
@@ -1826,8 +2040,8 @@ __export(remove_exports, {
1826
2040
  });
1827
2041
  import * as p6 from "@clack/prompts";
1828
2042
  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";
2043
+ import { join as join11, relative as relative3, dirname as dirname5 } from "path";
2044
+ import { unlink as unlink3, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
1831
2045
  import { existsSync as existsSync2 } from "fs";
1832
2046
  async function removeSingleComponent(installedKey, lock, config, cwd) {
1833
2047
  const entry = lock[installedKey];
@@ -1835,27 +2049,27 @@ async function removeSingleComponent(installedKey, lock, config, cwd) {
1835
2049
  const deleted = [];
1836
2050
  for (const filePath of entry.files) {
1837
2051
  try {
1838
- await unlink3(join10(cwd, filePath));
2052
+ await unlink3(join11(cwd, filePath));
1839
2053
  deleted.push(filePath);
1840
2054
  } catch {
1841
2055
  p6.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
1842
2056
  }
1843
2057
  }
1844
2058
  const baseDir = config.aliases.base ?? "src/ai";
1845
- const barrelPath = join10(cwd, baseDir, "index.ts");
1846
- const barrelDir = join10(cwd, baseDir);
2059
+ const barrelPath = join11(cwd, baseDir, "index.ts");
2060
+ const barrelDir = join11(cwd, baseDir);
1847
2061
  const barrelEligibleDirs = /* @__PURE__ */ new Set([
1848
2062
  config.aliases.agents,
1849
2063
  config.aliases.tools,
1850
2064
  config.aliases.skills
1851
2065
  ]);
1852
2066
  if (existsSync2(barrelPath) && deleted.length > 0) {
1853
- let barrelContent = await readFile7(barrelPath, "utf-8");
2067
+ let barrelContent = await readFile8(barrelPath, "utf-8");
1854
2068
  let barrelChanged = false;
1855
2069
  for (const filePath of deleted) {
1856
- const fileDir = dirname4(filePath);
2070
+ const fileDir = dirname5(filePath);
1857
2071
  if (!barrelEligibleDirs.has(fileDir)) continue;
1858
- const importPath = "./" + relative3(barrelDir, join10(cwd, filePath)).replace(/\\/g, "/");
2072
+ const importPath = "./" + relative3(barrelDir, join11(cwd, filePath)).replace(/\\/g, "/");
1859
2073
  const updated = removeImportFromBarrel(barrelContent, importPath);
1860
2074
  if (updated !== barrelContent) {
1861
2075
  barrelContent = updated;
@@ -1863,8 +2077,8 @@ async function removeSingleComponent(installedKey, lock, config, cwd) {
1863
2077
  }
1864
2078
  }
1865
2079
  if (barrelChanged) {
1866
- await writeFile8(barrelPath, barrelContent);
1867
- p6.log.info(`Updated barrel file: ${join10(baseDir, "index.ts")}`);
2080
+ await writeFile9(barrelPath, barrelContent);
2081
+ p6.log.info(`Updated barrel file: ${join11(baseDir, "index.ts")}`);
1868
2082
  }
1869
2083
  }
1870
2084
  delete lock[installedKey];
@@ -2012,6 +2226,19 @@ var init_update = __esm({
2012
2226
  }
2013
2227
  });
2014
2228
 
2229
+ // src/utils/naming.ts
2230
+ function toCamelCase(str) {
2231
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2232
+ }
2233
+ function toTitleCase(str) {
2234
+ return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2235
+ }
2236
+ var init_naming = __esm({
2237
+ "src/utils/naming.ts"() {
2238
+ "use strict";
2239
+ }
2240
+ });
2241
+
2015
2242
  // src/commands/create.ts
2016
2243
  var create_exports = {};
2017
2244
  __export(create_exports, {
@@ -2020,15 +2247,9 @@ __export(create_exports, {
2020
2247
  });
2021
2248
  import * as p8 from "@clack/prompts";
2022
2249
  import pc7 from "picocolors";
2023
- import { join as join11, relative as relative4 } from "path";
2250
+ import { join as join12, relative as relative4 } from "path";
2024
2251
  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
- }
2252
+ import { readFile as readFile9, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
2032
2253
  function generateAgentSource(name) {
2033
2254
  const camel = toCamelCase(name);
2034
2255
  return `import { registerAgent } from "@kitn/core";
@@ -2135,7 +2356,7 @@ async function createComponentInProject(type, name, opts) {
2135
2356
  const validType = type;
2136
2357
  const kitnType = typeToKitnType[validType];
2137
2358
  const fileName = validType === "skill" ? `${name}.md` : `${name}.ts`;
2138
- const filePath = join11(cwd, getInstallPath(config, kitnType, fileName));
2359
+ const filePath = join12(cwd, getInstallPath(config, kitnType, fileName));
2139
2360
  const dummyContent = "";
2140
2361
  const status = await checkFileStatus(filePath, dummyContent);
2141
2362
  if (status !== "new" /* New */) {
@@ -2163,18 +2384,18 @@ async function createComponentInProject(type, name, opts) {
2163
2384
  let barrelUpdated = false;
2164
2385
  if (BARREL_TYPES.includes(validType)) {
2165
2386
  const baseDir = config.aliases.base ?? "src/ai";
2166
- const barrelPath = join11(cwd, baseDir, "index.ts");
2387
+ const barrelPath = join12(cwd, baseDir, "index.ts");
2167
2388
  let barrelContent;
2168
2389
  if (existsSync3(barrelPath)) {
2169
- barrelContent = await readFile8(barrelPath, "utf-8");
2390
+ barrelContent = await readFile9(barrelPath, "utf-8");
2170
2391
  } else {
2171
2392
  barrelContent = createBarrelFile();
2172
- await mkdir5(join11(cwd, baseDir), { recursive: true });
2393
+ await mkdir6(join12(cwd, baseDir), { recursive: true });
2173
2394
  }
2174
- const importPath = "./" + relative4(join11(cwd, baseDir), filePath).replace(/\.ts$/, ".js");
2395
+ const importPath = "./" + relative4(join12(cwd, baseDir), filePath).replace(/\.ts$/, ".js");
2175
2396
  const updatedBarrel = addImportToBarrel(barrelContent, importPath);
2176
2397
  if (updatedBarrel !== barrelContent) {
2177
- await writeFile9(barrelPath, updatedBarrel);
2398
+ await writeFile10(barrelPath, updatedBarrel);
2178
2399
  barrelUpdated = true;
2179
2400
  }
2180
2401
  }
@@ -2202,6 +2423,7 @@ var init_create = __esm({
2202
2423
  "src/commands/create.ts"() {
2203
2424
  "use strict";
2204
2425
  init_config();
2426
+ init_naming();
2205
2427
  init_file_writer();
2206
2428
  init_barrel_manager();
2207
2429
  VALID_TYPES = ["agent", "tool", "skill", "storage", "cron"];
@@ -2216,36 +2438,661 @@ var init_create = __esm({
2216
2438
  }
2217
2439
  });
2218
2440
 
2441
+ // src/utils/component-resolver.ts
2442
+ import { readdir, readFile as readFile10 } from "fs/promises";
2443
+ import { join as join13, relative as relative5 } from "path";
2444
+ function stripSuffix(name, suffix) {
2445
+ if (name.endsWith(`-${suffix}`)) {
2446
+ return name.slice(0, -(suffix.length + 1));
2447
+ }
2448
+ return name;
2449
+ }
2450
+ function toolsDir(config, cwd) {
2451
+ const baseAlias = config.aliases.base ?? "src/ai";
2452
+ const tools = config.aliases.tools ?? join13(baseAlias, "tools");
2453
+ return join13(cwd, tools);
2454
+ }
2455
+ function agentsDir(config, cwd) {
2456
+ const baseAlias = config.aliases.base ?? "src/ai";
2457
+ const agents = config.aliases.agents ?? join13(baseAlias, "agents");
2458
+ return join13(cwd, agents);
2459
+ }
2460
+ async function findFile(dir, candidates) {
2461
+ for (const name of candidates) {
2462
+ const filePath = join13(dir, `${name}.ts`);
2463
+ try {
2464
+ await readFile10(filePath);
2465
+ return filePath;
2466
+ } catch {
2467
+ }
2468
+ }
2469
+ return null;
2470
+ }
2471
+ function parseExportName(source) {
2472
+ const match = source.match(/export\s+const\s+(\w+)/);
2473
+ return match ? match[1] : null;
2474
+ }
2475
+ function parseAgentName(source) {
2476
+ const match = source.match(/registerAgent\s*\(\s*\{[^}]*name:\s*"([^"]+)"/s);
2477
+ return match ? match[1] : null;
2478
+ }
2479
+ function computeImportPath(fromDir, toFile) {
2480
+ let rel = relative5(fromDir, toFile);
2481
+ if (!rel.startsWith(".")) {
2482
+ rel = `./${rel}`;
2483
+ }
2484
+ return rel.replace(/\.ts$/, ".js");
2485
+ }
2486
+ async function resolveToolByName(name, config, cwd) {
2487
+ const tDir = toolsDir(config, cwd);
2488
+ const aDir = agentsDir(config, cwd);
2489
+ const lock = await readLock(cwd);
2490
+ for (const [componentName, entry] of Object.entries(lock)) {
2491
+ if (componentName === name || componentName === `${name}-tool`) {
2492
+ const toolFile = entry.files.find((f) => {
2493
+ const toolsAlias = config.aliases.tools ?? join13(config.aliases.base ?? "src/ai", "tools");
2494
+ return f.startsWith(toolsAlias);
2495
+ });
2496
+ if (toolFile) {
2497
+ const filePath2 = join13(cwd, toolFile);
2498
+ try {
2499
+ const source2 = await readFile10(filePath2, "utf-8");
2500
+ const exportName2 = parseExportName(source2);
2501
+ if (exportName2) {
2502
+ return {
2503
+ filePath: filePath2,
2504
+ exportName: exportName2,
2505
+ importPath: computeImportPath(aDir, filePath2)
2506
+ };
2507
+ }
2508
+ } catch {
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+ const candidates = [name, stripSuffix(name, "tool")];
2514
+ const uniqueCandidates = [...new Set(candidates)];
2515
+ const filePath = await findFile(tDir, uniqueCandidates);
2516
+ if (!filePath) return null;
2517
+ const source = await readFile10(filePath, "utf-8");
2518
+ const exportName = parseExportName(source);
2519
+ if (!exportName) return null;
2520
+ return {
2521
+ filePath,
2522
+ exportName,
2523
+ importPath: computeImportPath(aDir, filePath)
2524
+ };
2525
+ }
2526
+ async function resolveAgentByName(name, config, cwd) {
2527
+ const aDir = agentsDir(config, cwd);
2528
+ const lock = await readLock(cwd);
2529
+ for (const [componentName, entry] of Object.entries(lock)) {
2530
+ if (componentName === name || componentName === `${name}-agent`) {
2531
+ const agentFile = entry.files.find((f) => {
2532
+ const agentsAlias = config.aliases.agents ?? join13(config.aliases.base ?? "src/ai", "agents");
2533
+ return f.startsWith(agentsAlias);
2534
+ });
2535
+ if (agentFile) {
2536
+ const filePath2 = join13(cwd, agentFile);
2537
+ try {
2538
+ const source2 = await readFile10(filePath2, "utf-8");
2539
+ const agentName2 = parseAgentName(source2);
2540
+ return {
2541
+ filePath: filePath2,
2542
+ name: agentName2 ?? componentName
2543
+ };
2544
+ } catch {
2545
+ }
2546
+ }
2547
+ }
2548
+ }
2549
+ const candidates = [name, stripSuffix(name, "agent")];
2550
+ const uniqueCandidates = [...new Set(candidates)];
2551
+ const filePath = await findFile(aDir, uniqueCandidates);
2552
+ if (!filePath) return null;
2553
+ const source = await readFile10(filePath, "utf-8");
2554
+ const agentName = parseAgentName(source);
2555
+ const fallbackName = filePath.split("/").pop().replace(/\.ts$/, "");
2556
+ return {
2557
+ filePath,
2558
+ name: agentName ?? fallbackName
2559
+ };
2560
+ }
2561
+ async function listTools(config, cwd) {
2562
+ const dir = toolsDir(config, cwd);
2563
+ return listComponentsInDir(dir);
2564
+ }
2565
+ async function listAgents(config, cwd) {
2566
+ const dir = agentsDir(config, cwd);
2567
+ return listComponentsInDir(dir);
2568
+ }
2569
+ async function listComponentsInDir(dir) {
2570
+ try {
2571
+ const entries = await readdir(dir);
2572
+ return entries.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts")).sort().map((f) => ({
2573
+ name: f.replace(/\.ts$/, ""),
2574
+ filePath: join13(dir, f)
2575
+ }));
2576
+ } catch {
2577
+ return [];
2578
+ }
2579
+ }
2580
+ var init_component_resolver = __esm({
2581
+ "src/utils/component-resolver.ts"() {
2582
+ "use strict";
2583
+ init_config();
2584
+ }
2585
+ });
2586
+
2587
+ // src/installers/agent-linker.ts
2588
+ function linkToolToAgent(content, tool, toolKey) {
2589
+ const key = toolKey ?? tool.exportName;
2590
+ const { exportName, importPath } = tool;
2591
+ const toolEntry = key === exportName ? exportName : `${key}: ${exportName}`;
2592
+ if (hasToolEntry(content, key)) {
2593
+ return { content, changed: false };
2594
+ }
2595
+ const importLine = `import { ${exportName} } from "${importPath}";`;
2596
+ let result = content;
2597
+ if (!result.includes(importLine)) {
2598
+ const lines = result.split("\n");
2599
+ let lastImportIndex = -1;
2600
+ for (let i = 0; i < lines.length; i++) {
2601
+ if (/^import\s+/.test(lines[i])) lastImportIndex = i;
2602
+ }
2603
+ if (lastImportIndex === -1) {
2604
+ result = `${importLine}
2605
+ ${result}`;
2606
+ } else {
2607
+ lines.splice(lastImportIndex + 1, 0, importLine);
2608
+ result = lines.join("\n");
2609
+ }
2610
+ }
2611
+ const insertResult = insertToolEntry(result, key, exportName);
2612
+ if (insertResult.error) {
2613
+ return {
2614
+ content,
2615
+ changed: false,
2616
+ error: `Could not auto-modify the agent file. Add manually:
2617
+ 1. Import: ${importLine}
2618
+ 2. Add to tools: { ${toolEntry} }`
2619
+ };
2620
+ }
2621
+ return { content: insertResult.content, changed: true };
2622
+ }
2623
+ function unlinkToolFromAgent(content, tool, toolKey) {
2624
+ const key = toolKey ?? tool.exportName;
2625
+ const { exportName, importPath } = tool;
2626
+ if (!hasToolEntry(content, key)) {
2627
+ return { content, changed: false };
2628
+ }
2629
+ const removeResult = removeToolEntry(content, key, exportName);
2630
+ if (removeResult.error) {
2631
+ return {
2632
+ content,
2633
+ changed: false,
2634
+ error: `Could not auto-modify the agent file. Remove manually:
2635
+ 1. Remove from tools: ${key === exportName ? key : `${key}: ${exportName}`}
2636
+ 2. Remove import if unused: import { ${exportName} } from "${importPath}";`
2637
+ };
2638
+ }
2639
+ let result = removeResult.content;
2640
+ if (!isExportNameReferenced(result, exportName)) {
2641
+ result = removeImportLine(result, exportName, importPath);
2642
+ }
2643
+ return { content: result, changed: true };
2644
+ }
2645
+ function hasToolEntry(content, key) {
2646
+ const toolsMatch = extractToolsBlock(content);
2647
+ if (!toolsMatch) return false;
2648
+ const toolsContent = toolsMatch.inner;
2649
+ const keyPattern = new RegExp(
2650
+ `(?:^|[,{\\s])${escapeRegex(key)}(?:\\s*[:,}\\s]|$)`
2651
+ );
2652
+ return keyPattern.test(toolsContent);
2653
+ }
2654
+ function extractToolsBlock(content) {
2655
+ const singleLine = /^([ \t]*)tools\s*:\s*\{([^}]*)\}/m;
2656
+ const singleMatch = singleLine.exec(content);
2657
+ if (singleMatch) {
2658
+ return {
2659
+ full: singleMatch[0],
2660
+ inner: singleMatch[2],
2661
+ startIndex: singleMatch.index,
2662
+ indent: singleMatch[1]
2663
+ };
2664
+ }
2665
+ const multiStart = /^([ \t]*)tools\s*:\s*\{/m;
2666
+ const multiMatch = multiStart.exec(content);
2667
+ if (!multiMatch) return null;
2668
+ const braceStart = multiMatch.index + multiMatch[0].length;
2669
+ let depth = 1;
2670
+ let i = braceStart;
2671
+ while (i < content.length && depth > 0) {
2672
+ if (content[i] === "{") depth++;
2673
+ else if (content[i] === "}") depth--;
2674
+ i++;
2675
+ }
2676
+ if (depth !== 0) return null;
2677
+ const full = content.slice(multiMatch.index, i);
2678
+ const inner = content.slice(braceStart, i - 1);
2679
+ return {
2680
+ full,
2681
+ inner,
2682
+ startIndex: multiMatch.index,
2683
+ indent: multiMatch[1]
2684
+ };
2685
+ }
2686
+ function insertToolEntry(content, key, exportName) {
2687
+ const block = extractToolsBlock(content);
2688
+ if (!block) return { content, error: "no tools block found" };
2689
+ const entry = key === exportName ? key : `${key}: ${exportName}`;
2690
+ const trimmedInner = block.inner.trim();
2691
+ let newToolsContent;
2692
+ if (trimmedInner === "") {
2693
+ newToolsContent = `tools: { ${entry} }`;
2694
+ } else if (!block.inner.includes("\n")) {
2695
+ const cleaned = trimmedInner.replace(/,?\s*$/, "");
2696
+ newToolsContent = `tools: { ${cleaned}, ${entry} }`;
2697
+ } else {
2698
+ const entryIndent = block.indent + " ";
2699
+ const existingTrimmed = block.inner.trimEnd();
2700
+ const withComma = existingTrimmed.endsWith(",") ? existingTrimmed : existingTrimmed + ",";
2701
+ newToolsContent = `tools: {
2702
+ ${withComma}
2703
+ ${entryIndent}${entry},
2704
+ ${block.indent}}`;
2705
+ }
2706
+ const newContent = content.replace(block.full, newToolsContent);
2707
+ return { content: newContent };
2708
+ }
2709
+ function removeToolEntry(content, key, exportName) {
2710
+ const block = extractToolsBlock(content);
2711
+ if (!block) return { content, error: "no tools block found" };
2712
+ const trimmedInner = block.inner.trim();
2713
+ if (!block.inner.includes("\n")) {
2714
+ const entries = trimmedInner.split(",").map((e) => e.trim()).filter((e) => e !== "");
2715
+ const filtered = entries.filter((e) => {
2716
+ const colonIdx = e.indexOf(":");
2717
+ const entryKey = colonIdx >= 0 ? e.slice(0, colonIdx).trim() : e.trim();
2718
+ return entryKey !== key;
2719
+ });
2720
+ const newInner = filtered.length === 0 ? "" : ` ${filtered.join(", ")} `;
2721
+ const newBlock = `tools: {${newInner}}`;
2722
+ return { content: content.replace(block.full, newBlock) };
2723
+ } else {
2724
+ const lines = block.inner.split("\n");
2725
+ const keyPattern = new RegExp(
2726
+ `^\\s*${escapeRegex(key)}\\s*(?::|,|$)`
2727
+ );
2728
+ const filtered = lines.filter((line) => !keyPattern.test(line));
2729
+ const hasEntries = filtered.some((l) => l.trim() !== "");
2730
+ if (!hasEntries) {
2731
+ const newBlock2 = `tools: {}`;
2732
+ return { content: content.replace(block.full, newBlock2) };
2733
+ }
2734
+ const cleanedLines = filtered.slice();
2735
+ for (let i = cleanedLines.length - 1; i >= 0; i--) {
2736
+ if (cleanedLines[i].trim() !== "") {
2737
+ if (!cleanedLines[i].trimEnd().endsWith(",")) {
2738
+ cleanedLines[i] = cleanedLines[i].trimEnd() + ",";
2739
+ }
2740
+ break;
2741
+ }
2742
+ }
2743
+ const newBlock = `tools: {
2744
+ ${cleanedLines.join("\n")}
2745
+ ${block.indent}}`;
2746
+ return { content: content.replace(block.full, newBlock) };
2747
+ }
2748
+ }
2749
+ function isExportNameReferenced(content, exportName) {
2750
+ const lines = content.split("\n");
2751
+ for (const line of lines) {
2752
+ if (/^import\s+/.test(line)) continue;
2753
+ const wordPattern = new RegExp(`\\b${escapeRegex(exportName)}\\b`);
2754
+ if (wordPattern.test(line)) return true;
2755
+ }
2756
+ return false;
2757
+ }
2758
+ function removeImportLine(content, exportName, importPath) {
2759
+ const lines = content.split("\n");
2760
+ const result = [];
2761
+ for (let i = 0; i < lines.length; i++) {
2762
+ const line = lines[i];
2763
+ const singleImportMatch = line.match(
2764
+ /^import\s*\{([^}]+)\}\s*from\s*["'](.+?)["']\s*;?\s*$/
2765
+ );
2766
+ if (singleImportMatch && singleImportMatch[2] === importPath) {
2767
+ const imports = singleImportMatch[1].split(",").map((s) => s.trim()).filter((s) => s !== "");
2768
+ if (imports.length === 1 && imports[0] === exportName) {
2769
+ if (i + 1 < lines.length && lines[i + 1].trim() === "") {
2770
+ i++;
2771
+ }
2772
+ continue;
2773
+ }
2774
+ const remaining = imports.filter((s) => s !== exportName);
2775
+ result.push(
2776
+ `import { ${remaining.join(", ")} } from "${importPath}";`
2777
+ );
2778
+ continue;
2779
+ }
2780
+ if (/^import\s*\{/.test(line) && !line.includes("}") && content.includes(importPath)) {
2781
+ const importLines = [line];
2782
+ let j = i + 1;
2783
+ while (j < lines.length && !lines[j].includes("}")) {
2784
+ importLines.push(lines[j]);
2785
+ j++;
2786
+ }
2787
+ if (j < lines.length) {
2788
+ importLines.push(lines[j]);
2789
+ }
2790
+ const fullImport = importLines.join("\n");
2791
+ if (fullImport.includes(importPath)) {
2792
+ const namesMatch = fullImport.match(/\{([^}]+)\}/);
2793
+ if (namesMatch) {
2794
+ const imports = namesMatch[1].split(",").map((s) => s.trim()).filter((s) => s !== "");
2795
+ if (imports.length === 1 && imports[0] === exportName) {
2796
+ i = j;
2797
+ if (i + 1 < lines.length && lines[i + 1].trim() === "") {
2798
+ i++;
2799
+ }
2800
+ continue;
2801
+ }
2802
+ const remaining = imports.filter((s) => s !== exportName);
2803
+ if (remaining.length <= 2) {
2804
+ result.push(
2805
+ `import { ${remaining.join(", ")} } from "${importPath}";`
2806
+ );
2807
+ } else {
2808
+ result.push(`import {`);
2809
+ remaining.forEach((name, idx) => {
2810
+ result.push(
2811
+ ` ${name}${idx < remaining.length - 1 ? "," : ""}`
2812
+ );
2813
+ });
2814
+ result.push(`} from "${importPath}";`);
2815
+ }
2816
+ i = j;
2817
+ continue;
2818
+ }
2819
+ }
2820
+ }
2821
+ result.push(line);
2822
+ }
2823
+ return result.join("\n");
2824
+ }
2825
+ function escapeRegex(s) {
2826
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2827
+ }
2828
+ var init_agent_linker = __esm({
2829
+ "src/installers/agent-linker.ts"() {
2830
+ "use strict";
2831
+ }
2832
+ });
2833
+
2834
+ // src/commands/link.ts
2835
+ var link_exports = {};
2836
+ __export(link_exports, {
2837
+ linkCommand: () => linkCommand
2838
+ });
2839
+ import * as p9 from "@clack/prompts";
2840
+ import pc8 from "picocolors";
2841
+ import { readFile as readFile11, writeFile as writeFile11 } from "fs/promises";
2842
+ import { basename } from "path";
2843
+ async function linkCommand(type, name, opts) {
2844
+ p9.intro(pc8.bgCyan(pc8.black(" kitn link ")));
2845
+ const cwd = process.cwd();
2846
+ const config = await readConfig(cwd);
2847
+ if (!config) {
2848
+ p9.log.error("No kitn.json found. Run `kitn init` first.");
2849
+ process.exit(1);
2850
+ }
2851
+ if (type && type !== "tool") {
2852
+ p9.log.error(
2853
+ `Unsupported type "${type}". Only ${pc8.bold("tool")} is supported.`
2854
+ );
2855
+ process.exit(1);
2856
+ }
2857
+ let toolName = name;
2858
+ if (!toolName) {
2859
+ const tools = await listTools(config, cwd);
2860
+ if (tools.length === 0) {
2861
+ p9.log.error("No tools found in your project.");
2862
+ process.exit(1);
2863
+ }
2864
+ const selected = await p9.select({
2865
+ message: "Select a tool to link:",
2866
+ options: tools.map((t) => ({
2867
+ value: t.name,
2868
+ label: t.name
2869
+ }))
2870
+ });
2871
+ if (p9.isCancel(selected)) {
2872
+ p9.cancel("Cancelled.");
2873
+ process.exit(0);
2874
+ }
2875
+ toolName = selected;
2876
+ }
2877
+ const tool = await resolveToolByName(toolName, config, cwd);
2878
+ if (!tool) {
2879
+ p9.log.error(
2880
+ `Tool "${toolName}" not found. Check that the file exists in your tools directory.`
2881
+ );
2882
+ process.exit(1);
2883
+ }
2884
+ let agentName = opts?.to;
2885
+ if (!agentName) {
2886
+ const agents = await listAgents(config, cwd);
2887
+ if (agents.length === 0) {
2888
+ p9.log.error("No agents found in your project.");
2889
+ process.exit(1);
2890
+ }
2891
+ const selected = await p9.select({
2892
+ message: "Select an agent to link the tool to:",
2893
+ options: agents.map((a) => ({
2894
+ value: a.name,
2895
+ label: a.name
2896
+ }))
2897
+ });
2898
+ if (p9.isCancel(selected)) {
2899
+ p9.cancel("Cancelled.");
2900
+ process.exit(0);
2901
+ }
2902
+ agentName = selected;
2903
+ }
2904
+ const agent = await resolveAgentByName(agentName, config, cwd);
2905
+ if (!agent) {
2906
+ p9.log.error(
2907
+ `Agent "${agentName}" not found. Check that the file exists in your agents directory.`
2908
+ );
2909
+ process.exit(1);
2910
+ }
2911
+ const agentContent = await readFile11(agent.filePath, "utf-8");
2912
+ const toolRef = {
2913
+ exportName: tool.exportName,
2914
+ importPath: tool.importPath
2915
+ };
2916
+ const result = linkToolToAgent(agentContent, toolRef, opts?.as);
2917
+ if (result.error) {
2918
+ p9.log.warn(result.error);
2919
+ p9.outro("Could not auto-link. Follow the manual instructions above.");
2920
+ process.exit(1);
2921
+ }
2922
+ if (!result.changed) {
2923
+ p9.log.info(
2924
+ `${pc8.cyan(tool.exportName)} is already linked to ${pc8.cyan(basename(agent.filePath))}.`
2925
+ );
2926
+ p9.outro("Nothing to do.");
2927
+ return;
2928
+ }
2929
+ await writeFile11(agent.filePath, result.content);
2930
+ p9.log.success(
2931
+ `Linked ${pc8.cyan(tool.exportName)} to ${pc8.cyan(basename(agent.filePath))}`
2932
+ );
2933
+ p9.log.message(
2934
+ ` ${pc8.green("+")} import { ${tool.exportName} } from "${tool.importPath}"`
2935
+ );
2936
+ p9.log.message(
2937
+ ` ${pc8.green("+")} tools: { ${opts?.as ? `${opts.as}: ${tool.exportName}` : tool.exportName} }`
2938
+ );
2939
+ p9.outro("Done!");
2940
+ }
2941
+ var init_link = __esm({
2942
+ "src/commands/link.ts"() {
2943
+ "use strict";
2944
+ init_config();
2945
+ init_component_resolver();
2946
+ init_agent_linker();
2947
+ }
2948
+ });
2949
+
2950
+ // src/commands/unlink.ts
2951
+ var unlink_exports = {};
2952
+ __export(unlink_exports, {
2953
+ unlinkCommand: () => unlinkCommand
2954
+ });
2955
+ import * as p10 from "@clack/prompts";
2956
+ import pc9 from "picocolors";
2957
+ import { readFile as readFile12, writeFile as writeFile12 } from "fs/promises";
2958
+ import { basename as basename2 } from "path";
2959
+ async function unlinkCommand(type, name, opts) {
2960
+ p10.intro(pc9.bgCyan(pc9.black(" kitn unlink ")));
2961
+ const cwd = process.cwd();
2962
+ const config = await readConfig(cwd);
2963
+ if (!config) {
2964
+ p10.log.error("No kitn.json found. Run `kitn init` first.");
2965
+ process.exit(1);
2966
+ }
2967
+ if (type && type !== "tool") {
2968
+ p10.log.error(
2969
+ `Unsupported type "${type}". Only ${pc9.bold("tool")} is supported.`
2970
+ );
2971
+ process.exit(1);
2972
+ }
2973
+ let toolName = name;
2974
+ if (!toolName) {
2975
+ const tools = await listTools(config, cwd);
2976
+ if (tools.length === 0) {
2977
+ p10.log.error("No tools found in your project.");
2978
+ process.exit(1);
2979
+ }
2980
+ const selected = await p10.select({
2981
+ message: "Select a tool to unlink:",
2982
+ options: tools.map((t) => ({
2983
+ value: t.name,
2984
+ label: t.name
2985
+ }))
2986
+ });
2987
+ if (p10.isCancel(selected)) {
2988
+ p10.cancel("Cancelled.");
2989
+ process.exit(0);
2990
+ }
2991
+ toolName = selected;
2992
+ }
2993
+ const tool = await resolveToolByName(toolName, config, cwd);
2994
+ if (!tool) {
2995
+ p10.log.error(
2996
+ `Tool "${toolName}" not found. Check that the file exists in your tools directory.`
2997
+ );
2998
+ process.exit(1);
2999
+ }
3000
+ let agentName = opts?.from;
3001
+ if (!agentName) {
3002
+ const agents = await listAgents(config, cwd);
3003
+ if (agents.length === 0) {
3004
+ p10.log.error("No agents found in your project.");
3005
+ process.exit(1);
3006
+ }
3007
+ const selected = await p10.select({
3008
+ message: "Select an agent to unlink the tool from:",
3009
+ options: agents.map((a) => ({
3010
+ value: a.name,
3011
+ label: a.name
3012
+ }))
3013
+ });
3014
+ if (p10.isCancel(selected)) {
3015
+ p10.cancel("Cancelled.");
3016
+ process.exit(0);
3017
+ }
3018
+ agentName = selected;
3019
+ }
3020
+ const agent = await resolveAgentByName(agentName, config, cwd);
3021
+ if (!agent) {
3022
+ p10.log.error(
3023
+ `Agent "${agentName}" not found. Check that the file exists in your agents directory.`
3024
+ );
3025
+ process.exit(1);
3026
+ }
3027
+ const agentContent = await readFile12(agent.filePath, "utf-8");
3028
+ const toolRef = {
3029
+ exportName: tool.exportName,
3030
+ importPath: tool.importPath
3031
+ };
3032
+ const result = unlinkToolFromAgent(agentContent, toolRef);
3033
+ if (result.error) {
3034
+ p10.log.warn(result.error);
3035
+ p10.outro("Could not auto-unlink. Follow the manual instructions above.");
3036
+ process.exit(1);
3037
+ }
3038
+ if (!result.changed) {
3039
+ p10.log.info(
3040
+ `${pc9.cyan(tool.exportName)} is not linked to ${pc9.cyan(basename2(agent.filePath))}.`
3041
+ );
3042
+ p10.outro("Nothing to do.");
3043
+ return;
3044
+ }
3045
+ await writeFile12(agent.filePath, result.content);
3046
+ p10.log.success(
3047
+ `Unlinked ${pc9.cyan(tool.exportName)} from ${pc9.cyan(basename2(agent.filePath))}`
3048
+ );
3049
+ p10.log.message(
3050
+ ` ${pc9.red("-")} import { ${tool.exportName} } from "${tool.importPath}"`
3051
+ );
3052
+ p10.log.message(
3053
+ ` ${pc9.red("-")} tools: { ${tool.exportName} }`
3054
+ );
3055
+ p10.outro("Done!");
3056
+ }
3057
+ var init_unlink = __esm({
3058
+ "src/commands/unlink.ts"() {
3059
+ "use strict";
3060
+ init_config();
3061
+ init_component_resolver();
3062
+ init_agent_linker();
3063
+ }
3064
+ });
3065
+
2219
3066
  // src/commands/info.ts
2220
3067
  var info_exports = {};
2221
3068
  __export(info_exports, {
2222
3069
  infoCommand: () => infoCommand
2223
3070
  });
2224
- import * as p9 from "@clack/prompts";
2225
- import pc8 from "picocolors";
3071
+ import * as p11 from "@clack/prompts";
3072
+ import pc10 from "picocolors";
2226
3073
  async function infoCommand(component) {
2227
3074
  const cwd = process.cwd();
2228
3075
  const config = await readConfig(cwd);
2229
3076
  if (!config) {
2230
- p9.log.error("No kitn.json found. Run `kitn init` first.");
3077
+ p11.log.error("No kitn.json found. Run `kitn init` first.");
2231
3078
  process.exit(1);
2232
3079
  }
2233
3080
  const ref = parseComponentRef(component);
2234
3081
  const fetcher = new RegistryFetcher(config.registries);
2235
- const s = p9.spinner();
3082
+ const s = p11.spinner();
2236
3083
  s.start("Fetching component info...");
2237
3084
  let index;
2238
3085
  try {
2239
3086
  index = await fetcher.fetchIndex(ref.namespace);
2240
3087
  } catch (err) {
2241
- s.stop(pc8.red("Failed to fetch registry"));
2242
- p9.log.error(err.message);
3088
+ s.stop(pc10.red("Failed to fetch registry"));
3089
+ p11.log.error(err.message);
2243
3090
  process.exit(1);
2244
3091
  }
2245
3092
  const indexItem = index.items.find((i) => i.name === ref.name);
2246
3093
  if (!indexItem) {
2247
- s.stop(pc8.red("Component not found"));
2248
- p9.log.error(`Component '${ref.name}' not found in registry.`);
3094
+ s.stop(pc10.red("Component not found"));
3095
+ p11.log.error(`Component '${ref.name}' not found in registry.`);
2249
3096
  process.exit(1);
2250
3097
  }
2251
3098
  const dir = typeToDir[indexItem.type];
@@ -2253,8 +3100,8 @@ async function infoCommand(component) {
2253
3100
  try {
2254
3101
  item = await fetcher.fetchItem(ref.name, dir, ref.namespace, ref.version);
2255
3102
  } catch (err) {
2256
- s.stop(pc8.red("Failed to fetch component"));
2257
- p9.log.error(err.message);
3103
+ s.stop(pc10.red("Failed to fetch component"));
3104
+ p11.log.error(err.message);
2258
3105
  process.exit(1);
2259
3106
  }
2260
3107
  s.stop("Component found");
@@ -2262,63 +3109,63 @@ async function infoCommand(component) {
2262
3109
  const typeName = indexItem.type.replace("kitn:", "");
2263
3110
  console.log();
2264
3111
  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)}`
3112
+ ` ${pc10.bold(item.name)} ${pc10.cyan(`v${version}`)}${" ".repeat(Math.max(1, 40 - item.name.length - version.length - 2))}${pc10.dim(ref.namespace)}`
2266
3113
  );
2267
- console.log(` ${pc8.dim(item.description)}`);
3114
+ console.log(` ${pc10.dim(item.description)}`);
2268
3115
  console.log();
2269
- console.log(` ${pc8.dim("Type:")} ${typeName}`);
3116
+ console.log(` ${pc10.dim("Type:")} ${typeName}`);
2270
3117
  if (item.dependencies?.length) {
2271
3118
  console.log(
2272
- ` ${pc8.dim("Dependencies:")} ${item.dependencies.join(", ")}`
3119
+ ` ${pc10.dim("Dependencies:")} ${item.dependencies.join(", ")}`
2273
3120
  );
2274
3121
  }
2275
3122
  if (item.registryDependencies?.length) {
2276
3123
  console.log(
2277
- ` ${pc8.dim("Registry deps:")} ${item.registryDependencies.join(", ")}`
3124
+ ` ${pc10.dim("Registry deps:")} ${item.registryDependencies.join(", ")}`
2278
3125
  );
2279
3126
  }
2280
3127
  if (item.categories?.length) {
2281
3128
  console.log(
2282
- ` ${pc8.dim("Categories:")} ${item.categories.join(", ")}`
3129
+ ` ${pc10.dim("Categories:")} ${item.categories.join(", ")}`
2283
3130
  );
2284
3131
  }
2285
3132
  if (item.updatedAt) {
2286
- console.log(` ${pc8.dim("Updated:")} ${item.updatedAt}`);
3133
+ console.log(` ${pc10.dim("Updated:")} ${item.updatedAt}`);
2287
3134
  }
2288
3135
  const versions = indexItem.versions;
2289
3136
  if (versions?.length) {
2290
- console.log(` ${pc8.dim("Versions:")} ${versions.join(", ")}`);
3137
+ console.log(` ${pc10.dim("Versions:")} ${versions.join(", ")}`);
2291
3138
  }
2292
3139
  if (item.changelog?.length) {
2293
3140
  console.log();
2294
- console.log(` ${pc8.bold("Changelog:")}`);
3141
+ console.log(` ${pc10.bold("Changelog:")}`);
2295
3142
  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);
3143
+ 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
3144
  console.log(
2298
- ` ${pc8.cyan(entry.version)} ${pc8.dim(entry.date)} ${tag} ${entry.note}`
3145
+ ` ${pc10.cyan(entry.version)} ${pc10.dim(entry.date)} ${tag} ${entry.note}`
2299
3146
  );
2300
3147
  }
2301
3148
  }
2302
3149
  console.log();
2303
3150
  const fileCount = item.files.length;
2304
- console.log(` ${pc8.bold(`Files:`)} ${pc8.dim(`(${fileCount})`)}`);
3151
+ console.log(` ${pc10.bold(`Files:`)} ${pc10.dim(`(${fileCount})`)}`);
2305
3152
  const maxShown = 10;
2306
3153
  for (const file of item.files.slice(0, maxShown)) {
2307
- console.log(` ${pc8.dim(file.path)}`);
3154
+ console.log(` ${pc10.dim(file.path)}`);
2308
3155
  }
2309
3156
  if (fileCount > maxShown) {
2310
- console.log(` ${pc8.dim(`... and ${fileCount - maxShown} more`)}`);
3157
+ console.log(` ${pc10.dim(`... and ${fileCount - maxShown} more`)}`);
2311
3158
  }
2312
3159
  const lock = await readLock(cwd);
2313
3160
  const installed = lock[item.name];
2314
3161
  if (installed) {
2315
3162
  console.log();
2316
3163
  console.log(
2317
- ` ${pc8.green("Installed")} ${pc8.dim(`v${installed.version}`)}`
3164
+ ` ${pc10.green("Installed")} ${pc10.dim(`v${installed.version}`)}`
2318
3165
  );
2319
3166
  if (version !== installed.version) {
2320
3167
  console.log(
2321
- ` ${pc8.yellow("Update available:")} ${pc8.dim(`v${installed.version}`)} \u2192 ${pc8.cyan(`v${version}`)}`
3168
+ ` ${pc10.yellow("Update available:")} ${pc10.dim(`v${installed.version}`)} \u2192 ${pc10.cyan(`v${version}`)}`
2322
3169
  );
2323
3170
  }
2324
3171
  }
@@ -2340,41 +3187,41 @@ __export(check_exports, {
2340
3187
  checkCommand: () => checkCommand
2341
3188
  });
2342
3189
  import { execSync as execSync2 } from "child_process";
2343
- import * as p10 from "@clack/prompts";
2344
- import pc9 from "picocolors";
3190
+ import * as p12 from "@clack/prompts";
3191
+ import pc11 from "picocolors";
2345
3192
  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();
3193
+ p12.intro(pc11.bgCyan(pc11.black(" kitn check ")));
3194
+ p12.log.info(`kitn v${currentVersion}`);
3195
+ const s = p12.spinner();
2349
3196
  s.start("Checking for updates...");
2350
3197
  const latest = await fetchLatestVersion();
2351
3198
  if (!latest) {
2352
- s.stop(pc9.yellow("Could not reach the npm registry"));
2353
- p10.outro("Try again later.");
3199
+ s.stop(pc11.yellow("Could not reach the npm registry"));
3200
+ p12.outro("Try again later.");
2354
3201
  return;
2355
3202
  }
2356
3203
  if (isNewer(latest, currentVersion)) {
2357
- s.stop(pc9.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
3204
+ s.stop(pc11.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
2358
3205
  const pm = detectCliInstaller();
2359
3206
  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)}`);
3207
+ const shouldUpdate = await p12.confirm({ message: "Update now?" });
3208
+ if (p12.isCancel(shouldUpdate) || !shouldUpdate) {
3209
+ p12.log.message(` Run: ${pc11.cyan(installCmd)}`);
2363
3210
  } else {
2364
- const us = p10.spinner();
3211
+ const us = p12.spinner();
2365
3212
  us.start("Updating...");
2366
3213
  try {
2367
3214
  execSync2(installCmd, { stdio: "pipe" });
2368
- us.stop(pc9.green(`Updated to v${latest}`));
3215
+ us.stop(pc11.green(`Updated to v${latest}`));
2369
3216
  } catch {
2370
- us.stop(pc9.red("Update failed"));
2371
- p10.log.message(` Run manually: ${pc9.cyan(installCmd)}`);
3217
+ us.stop(pc11.red("Update failed"));
3218
+ p12.log.message(` Run manually: ${pc11.cyan(installCmd)}`);
2372
3219
  }
2373
3220
  }
2374
3221
  } else {
2375
- s.stop(pc9.green("You're on the latest version"));
3222
+ s.stop(pc11.green("You're on the latest version"));
2376
3223
  }
2377
- p10.outro("");
3224
+ p12.outro("");
2378
3225
  }
2379
3226
  var init_check = __esm({
2380
3227
  "src/commands/check.ts"() {
@@ -2384,6 +3231,64 @@ var init_check = __esm({
2384
3231
  }
2385
3232
  });
2386
3233
 
3234
+ // src/commands/rules.ts
3235
+ var rules_exports = {};
3236
+ __export(rules_exports, {
3237
+ rulesCommand: () => rulesCommand
3238
+ });
3239
+ import * as p13 from "@clack/prompts";
3240
+ import pc12 from "picocolors";
3241
+ async function rulesCommand() {
3242
+ p13.intro(pc12.bgCyan(pc12.black(" kitn rules ")));
3243
+ const cwd = process.cwd();
3244
+ const config = await readConfig(cwd);
3245
+ if (!config) {
3246
+ p13.log.error(`No kitn.json found. Run ${pc12.bold("kitn init")} first.`);
3247
+ process.exit(1);
3248
+ }
3249
+ let selectedIds = config.aiTools;
3250
+ if (!selectedIds || selectedIds.length === 0) {
3251
+ const rulesConfig = await fetchRulesConfig(config.registries);
3252
+ const selected = await p13.multiselect({
3253
+ message: "Which AI coding tools do you use?",
3254
+ options: rulesConfig.tools.map((t) => ({
3255
+ value: t.id,
3256
+ label: t.name,
3257
+ hint: t.description
3258
+ })),
3259
+ required: false
3260
+ });
3261
+ if (p13.isCancel(selected)) {
3262
+ p13.cancel("Cancelled.");
3263
+ process.exit(0);
3264
+ }
3265
+ selectedIds = selected;
3266
+ if (selectedIds.length === 0) {
3267
+ p13.log.warn("No tools selected. Nothing to generate.");
3268
+ p13.outro("Done.");
3269
+ return;
3270
+ }
3271
+ const updatedConfig = { ...config, aiTools: selectedIds };
3272
+ await writeConfig(cwd, updatedConfig);
3273
+ p13.log.info(`Saved tool selections to ${pc12.bold("kitn.json")}`);
3274
+ }
3275
+ const s = p13.spinner();
3276
+ s.start("Generating rules files");
3277
+ const written = await generateRulesFiles(cwd, config, selectedIds);
3278
+ s.stop("Rules files generated");
3279
+ for (const filePath of written) {
3280
+ p13.log.success(`${pc12.green("+")} ${filePath}`);
3281
+ }
3282
+ p13.outro(`Generated ${written.length} rules file${written.length === 1 ? "" : "s"}.`);
3283
+ }
3284
+ var init_rules = __esm({
3285
+ "src/commands/rules.ts"() {
3286
+ "use strict";
3287
+ init_config();
3288
+ init_rules_generator();
3289
+ }
3290
+ });
3291
+
2387
3292
  // src/commands/registry.ts
2388
3293
  var registry_exports = {};
2389
3294
  __export(registry_exports, {
@@ -2391,8 +3296,8 @@ __export(registry_exports, {
2391
3296
  registryListCommand: () => registryListCommand,
2392
3297
  registryRemoveCommand: () => registryRemoveCommand
2393
3298
  });
2394
- import * as p11 from "@clack/prompts";
2395
- import pc10 from "picocolors";
3299
+ import * as p14 from "@clack/prompts";
3300
+ import pc13 from "picocolors";
2396
3301
  async function registryAddCommand(namespace, url, opts = {}) {
2397
3302
  const cwd = opts.cwd ?? process.cwd();
2398
3303
  const config = await readConfig(cwd);
@@ -2418,10 +3323,10 @@ async function registryAddCommand(namespace, url, opts = {}) {
2418
3323
  config.registries[namespace] = url;
2419
3324
  }
2420
3325
  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}`));
3326
+ p14.log.success(`Added registry ${pc13.bold(namespace)}`);
3327
+ p14.log.message(pc13.dim(` ${url}`));
3328
+ if (opts.homepage) p14.log.message(pc13.dim(` Homepage: ${opts.homepage}`));
3329
+ if (opts.description) p14.log.message(pc13.dim(` ${opts.description}`));
2425
3330
  }
2426
3331
  async function registryRemoveCommand(namespace, opts = {}) {
2427
3332
  const cwd = opts.cwd ?? process.cwd();
@@ -2442,10 +3347,10 @@ async function registryRemoveCommand(namespace, opts = {}) {
2442
3347
  }
2443
3348
  delete config.registries[namespace];
2444
3349
  await writeConfig(cwd, config);
2445
- p11.log.success(`Removed registry ${pc10.bold(namespace)}`);
3350
+ p14.log.success(`Removed registry ${pc13.bold(namespace)}`);
2446
3351
  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"));
3352
+ p14.log.warn(`${affectedComponents.length} installed component(s) referenced this registry:
3353
+ ` + affectedComponents.map((name) => ` ${pc13.yellow("!")} ${name}`).join("\n"));
2449
3354
  }
2450
3355
  return { affectedComponents };
2451
3356
  }
@@ -2460,15 +3365,15 @@ async function registryListCommand(opts = {}) {
2460
3365
  return { namespace, url, homepage, description };
2461
3366
  });
2462
3367
  if (entries.length === 0) {
2463
- p11.log.message(pc10.dim(" No registries configured."));
3368
+ p14.log.message(pc13.dim(" No registries configured."));
2464
3369
  } else {
2465
3370
  const lines = [];
2466
3371
  for (const { namespace, url, homepage, description } of entries) {
2467
- lines.push(` ${pc10.bold(namespace.padEnd(16))} ${pc10.dim(url)}`);
3372
+ lines.push(` ${pc13.bold(namespace.padEnd(16))} ${pc13.dim(url)}`);
2468
3373
  if (description) lines.push(` ${" ".repeat(16)} ${description}`);
2469
- if (homepage) lines.push(` ${" ".repeat(16)} ${pc10.dim(homepage)}`);
3374
+ if (homepage) lines.push(` ${" ".repeat(16)} ${pc13.dim(homepage)}`);
2470
3375
  }
2471
- p11.log.message(lines.join("\n"));
3376
+ p14.log.message(lines.join("\n"));
2472
3377
  }
2473
3378
  return entries;
2474
3379
  }
@@ -2482,7 +3387,7 @@ var init_registry = __esm({
2482
3387
  // src/index.ts
2483
3388
  init_update_check();
2484
3389
  import { Command } from "commander";
2485
- var VERSION = true ? "0.1.30" : "0.0.0-dev";
3390
+ var VERSION = true ? "0.1.32" : "0.0.0-dev";
2486
3391
  var printUpdateNotice = startUpdateCheck(VERSION);
2487
3392
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
2488
3393
  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 +3418,14 @@ program.command("create").description("Scaffold a new kitn component").argument(
2513
3418
  const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
2514
3419
  await createCommand2(type, name);
2515
3420
  });
3421
+ 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) => {
3422
+ const { linkCommand: linkCommand2 } = await Promise.resolve().then(() => (init_link(), link_exports));
3423
+ await linkCommand2(type, name, opts);
3424
+ });
3425
+ 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) => {
3426
+ const { unlinkCommand: unlinkCommand2 } = await Promise.resolve().then(() => (init_unlink(), unlink_exports));
3427
+ await unlinkCommand2(type, name, opts);
3428
+ });
2516
3429
  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
3430
  const { infoCommand: infoCommand2 } = await Promise.resolve().then(() => (init_info(), info_exports));
2518
3431
  await infoCommand2(component);
@@ -2521,6 +3434,10 @@ program.command("check").description("Check for CLI updates").action(async () =>
2521
3434
  const { checkCommand: checkCommand2 } = await Promise.resolve().then(() => (init_check(), check_exports));
2522
3435
  await checkCommand2(VERSION);
2523
3436
  });
3437
+ program.command("rules").description("Regenerate AI coding tool rules files").action(async () => {
3438
+ const { rulesCommand: rulesCommand2 } = await Promise.resolve().then(() => (init_rules(), rules_exports));
3439
+ await rulesCommand2();
3440
+ });
2524
3441
  var registry = program.command("registry").description("Manage component registries");
2525
3442
  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
3443
  const { registryAddCommand: registryAddCommand2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));