@kitnai/cli 0.1.22 → 0.1.24

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
@@ -9,9 +9,72 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/utils/detect.ts
13
+ import { access } from "fs/promises";
14
+ import { join } from "path";
15
+ async function detectPackageManager(dir) {
16
+ for (const [lockfile, pm] of LOCKFILE_MAP) {
17
+ try {
18
+ await access(join(dir, lockfile));
19
+ return pm;
20
+ } catch {
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ function getRunCommand(pm) {
26
+ switch (pm) {
27
+ case "bun":
28
+ return "bunx";
29
+ case "pnpm":
30
+ return "pnpm dlx";
31
+ case "yarn":
32
+ return "yarn dlx";
33
+ case "npm":
34
+ return "npx";
35
+ }
36
+ }
37
+ function detectCliInstaller() {
38
+ const userAgent = process.env.npm_config_user_agent ?? "";
39
+ if (userAgent.startsWith("bun/")) return "bun";
40
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
41
+ if (userAgent.startsWith("yarn/")) return "yarn";
42
+ if (userAgent.startsWith("npm/")) return "npm";
43
+ const invoker = process.env._ ?? "";
44
+ if (invoker.includes("bun")) return "bun";
45
+ if (invoker.includes("pnpm")) return "pnpm";
46
+ if (invoker.includes("yarn")) return "yarn";
47
+ return "npm";
48
+ }
49
+ function getGlobalInstallCommand(pm, pkg) {
50
+ switch (pm) {
51
+ case "bun":
52
+ return `bun install -g ${pkg}`;
53
+ case "pnpm":
54
+ return `pnpm add -g ${pkg}`;
55
+ case "yarn":
56
+ return `yarn global add ${pkg}`;
57
+ case "npm":
58
+ return `npm install -g ${pkg}`;
59
+ }
60
+ }
61
+ var LOCKFILE_MAP;
62
+ var init_detect = __esm({
63
+ "src/utils/detect.ts"() {
64
+ "use strict";
65
+ LOCKFILE_MAP = [
66
+ ["bun.lock", "bun"],
67
+ ["bun.lockb", "bun"],
68
+ ["pnpm-lock.yaml", "pnpm"],
69
+ ["yarn.lock", "yarn"],
70
+ ["package-lock.json", "npm"]
71
+ ];
72
+ }
73
+ });
74
+
12
75
  // src/utils/update-check.ts
13
76
  import { readFile, writeFile, mkdir } from "fs/promises";
14
- import { join } from "path";
77
+ import { join as join2 } from "path";
15
78
  import { homedir } from "os";
16
79
  import pc from "picocolors";
17
80
  async function readCache() {
@@ -65,10 +128,13 @@ function startUpdateCheck(currentVersion) {
65
128
  }
66
129
  }
67
130
  if (latest && isNewer(latest, currentVersion)) {
131
+ const pm = detectCliInstaller();
132
+ const runCmd = `${getRunCommand(pm)} @kitnai/cli@latest`;
133
+ const installCmd = getGlobalInstallCommand(pm, "@kitnai/cli");
68
134
  message = [
69
135
  "",
70
136
  pc.yellow(` Update available: ${pc.dim(currentVersion)} \u2192 ${pc.green(latest)}`),
71
- pc.dim(` Run ${pc.cyan("npx @kitnai/cli@latest")} or ${pc.cyan("npm i -g @kitnai/cli")}`),
137
+ pc.dim(` Run ${pc.cyan(runCmd)} or ${pc.cyan(installCmd)}`),
72
138
  ""
73
139
  ].join("\n");
74
140
  }
@@ -83,15 +149,16 @@ var CACHE_DIR, CACHE_FILE, CHECK_INTERVAL;
83
149
  var init_update_check = __esm({
84
150
  "src/utils/update-check.ts"() {
85
151
  "use strict";
86
- CACHE_DIR = join(homedir(), ".kitn");
87
- CACHE_FILE = join(CACHE_DIR, "update-check.json");
152
+ init_detect();
153
+ CACHE_DIR = join2(homedir(), ".kitn");
154
+ CACHE_FILE = join2(CACHE_DIR, "update-check.json");
88
155
  CHECK_INTERVAL = 60 * 60 * 1e3;
89
156
  }
90
157
  });
91
158
 
92
159
  // src/utils/config.ts
93
160
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
94
- import { join as join2 } from "path";
161
+ import { join as join3 } from "path";
95
162
  import { z } from "zod";
96
163
  function getRegistryUrl(entry) {
97
164
  return typeof entry === "string" ? entry : entry.url;
@@ -102,7 +169,7 @@ function resolveRoutesAlias(config) {
102
169
  }
103
170
  async function readConfig(projectDir) {
104
171
  try {
105
- const raw = await readFile2(join2(projectDir, CONFIG_FILE), "utf-8");
172
+ const raw = await readFile2(join3(projectDir, CONFIG_FILE), "utf-8");
106
173
  return configSchema.parse(JSON.parse(raw));
107
174
  } catch {
108
175
  return null;
@@ -110,16 +177,16 @@ async function readConfig(projectDir) {
110
177
  }
111
178
  async function writeConfig(projectDir, config) {
112
179
  const data = { $schema: "https://kitn.dev/schema/config.json", ...config };
113
- await writeFile2(join2(projectDir, CONFIG_FILE), JSON.stringify(data, null, 2) + "\n");
180
+ await writeFile2(join3(projectDir, CONFIG_FILE), JSON.stringify(data, null, 2) + "\n");
114
181
  }
115
182
  function getInstallPath(config, type, fileName, namespace) {
116
183
  const aliasKey = typeToAliasKey[type];
117
184
  const base = config.aliases[aliasKey];
118
185
  if (namespace && namespace !== "@kitn") {
119
186
  const nsDir = namespace.replace("@", "");
120
- return join2(base, nsDir, fileName);
187
+ return join3(base, nsDir, fileName);
121
188
  }
122
- return join2(base, fileName);
189
+ return join3(base, fileName);
123
190
  }
124
191
  var componentType, installedComponentSchema, registryEntrySchema, registryValueSchema, configSchema, FRAMEWORK_TO_ADAPTER, CONFIG_FILE, typeToAliasKey;
125
192
  var init_config = __esm({
@@ -128,10 +195,13 @@ var init_config = __esm({
128
195
  componentType = z.enum(["kitn:agent", "kitn:tool", "kitn:skill", "kitn:storage", "kitn:package"]);
129
196
  installedComponentSchema = z.object({
130
197
  registry: z.string().optional(),
198
+ type: componentType,
199
+ slot: z.string().optional(),
131
200
  version: z.string(),
132
201
  installedAt: z.string(),
133
202
  files: z.array(z.string()),
134
- hash: z.string()
203
+ hash: z.string(),
204
+ registryDependencies: z.array(z.string()).optional()
135
205
  });
136
206
  registryEntrySchema = z.object({
137
207
  url: z.string(),
@@ -142,7 +212,7 @@ var init_config = __esm({
142
212
  configSchema = z.object({
143
213
  $schema: z.string().optional(),
144
214
  runtime: z.enum(["bun", "node", "deno"]),
145
- framework: z.enum(["hono", "cloudflare", "elysia", "fastify", "express"]).optional(),
215
+ framework: z.enum(["hono", "hono-openapi", "cloudflare", "elysia", "fastify", "express"]).optional(),
146
216
  aliases: z.object({
147
217
  base: z.string().optional(),
148
218
  agents: z.string(),
@@ -155,6 +225,7 @@ var init_config = __esm({
155
225
  });
156
226
  FRAMEWORK_TO_ADAPTER = {
157
227
  hono: "hono",
228
+ "hono-openapi": "hono-openapi",
158
229
  elysia: "elysia"
159
230
  };
160
231
  CONFIG_FILE = "kitn.json";
@@ -169,7 +240,7 @@ var init_config = __esm({
169
240
 
170
241
  // src/installers/tsconfig-patcher.ts
171
242
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
172
- import { join as join3 } from "path";
243
+ import { join as join4 } from "path";
173
244
  function stripJsonc(text3) {
174
245
  return text3.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
175
246
  }
@@ -208,7 +279,7 @@ function patchTsconfig(tsconfigContent, paths, removePrefixes) {
208
279
  return JSON.stringify(config, null, 2) + "\n";
209
280
  }
210
281
  async function patchProjectTsconfig(projectDir, paths, removePrefixes) {
211
- const tsconfigPath = join3(projectDir, "tsconfig.json");
282
+ const tsconfigPath = join4(projectDir, "tsconfig.json");
212
283
  let content;
213
284
  try {
214
285
  content = await readFile3(tsconfigPath, "utf-8");
@@ -258,33 +329,6 @@ var init_barrel_manager = __esm({
258
329
  }
259
330
  });
260
331
 
261
- // src/utils/detect.ts
262
- import { access } from "fs/promises";
263
- import { join as join4 } from "path";
264
- async function detectPackageManager(dir) {
265
- for (const [lockfile, pm] of LOCKFILE_MAP) {
266
- try {
267
- await access(join4(dir, lockfile));
268
- return pm;
269
- } catch {
270
- }
271
- }
272
- return null;
273
- }
274
- var LOCKFILE_MAP;
275
- var init_detect = __esm({
276
- "src/utils/detect.ts"() {
277
- "use strict";
278
- LOCKFILE_MAP = [
279
- ["bun.lock", "bun"],
280
- ["bun.lockb", "bun"],
281
- ["pnpm-lock.yaml", "pnpm"],
282
- ["yarn.lock", "yarn"],
283
- ["package-lock.json", "npm"]
284
- ];
285
- }
286
- });
287
-
288
332
  // src/registry/fetcher.ts
289
333
  function urlOf(entry) {
290
334
  return typeof entry === "string" ? entry : entry.url;
@@ -728,6 +772,7 @@ var init_schema = __esm({
728
772
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
729
773
  envVars: z2.record(z2.string(), envVarConfigSchema).optional(),
730
774
  categories: z2.array(z2.string()).optional(),
775
+ slot: z2.string().optional(),
731
776
  docs: z2.string().optional(),
732
777
  changelog: z2.array(changelogEntrySchema).optional()
733
778
  });
@@ -745,6 +790,7 @@ var init_schema = __esm({
745
790
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
746
791
  docs: z2.string().optional(),
747
792
  categories: z2.array(z2.string()).optional(),
793
+ slot: z2.string().optional(),
748
794
  version: z2.string().optional(),
749
795
  updatedAt: z2.string().optional(),
750
796
  changelog: z2.array(changelogEntrySchema).optional()
@@ -755,6 +801,7 @@ var init_schema = __esm({
755
801
  description: z2.string(),
756
802
  registryDependencies: z2.array(z2.string()).optional(),
757
803
  categories: z2.array(z2.string()).optional(),
804
+ slot: z2.string().optional(),
758
805
  version: z2.string().optional(),
759
806
  versions: z2.array(z2.string()).optional(),
760
807
  updatedAt: z2.string().optional()
@@ -781,9 +828,9 @@ __export(add_exports, {
781
828
  });
782
829
  import * as p2 from "@clack/prompts";
783
830
  import pc3 from "picocolors";
784
- import { join as join7 } from "path";
831
+ import { join as join7, dirname as dirname3 } from "path";
785
832
  import { existsSync } from "fs";
786
- import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir3 } from "fs/promises";
833
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir3, unlink } from "fs/promises";
787
834
  import { relative as relative2 } from "path";
788
835
  async function addCommand(components, opts) {
789
836
  p2.intro(pc3.bgCyan(pc3.black(" kitn add ")));
@@ -794,8 +841,63 @@ async function addCommand(components, opts) {
794
841
  process.exit(1);
795
842
  }
796
843
  if (components.length === 0) {
797
- p2.log.error("Please specify at least one component to add.");
798
- process.exit(1);
844
+ const fetcher2 = new RegistryFetcher(config.registries);
845
+ const s2 = p2.spinner();
846
+ s2.start("Fetching registry...");
847
+ const allItems = [];
848
+ for (const namespace of Object.keys(config.registries)) {
849
+ try {
850
+ const index = await fetcher2.fetchIndex(namespace);
851
+ for (const item of index.items) {
852
+ allItems.push({ name: item.name, type: item.type, description: item.description, namespace });
853
+ }
854
+ } catch {
855
+ }
856
+ }
857
+ s2.stop(`Found ${allItems.length} component(s)`);
858
+ if (allItems.length === 0) {
859
+ p2.log.warn("No components found in configured registries.");
860
+ process.exit(0);
861
+ }
862
+ const installed = new Set(Object.keys(config.installed ?? {}));
863
+ const typeLabels = {
864
+ "kitn:agent": "Agents",
865
+ "kitn:tool": "Tools",
866
+ "kitn:skill": "Skills",
867
+ "kitn:storage": "Storage",
868
+ "kitn:package": "Packages"
869
+ };
870
+ const groups = /* @__PURE__ */ new Map();
871
+ for (const item of allItems) {
872
+ if (!groups.has(item.type)) groups.set(item.type, []);
873
+ groups.get(item.type).push(item);
874
+ }
875
+ const options = [];
876
+ for (const [type, items] of groups) {
877
+ const label = typeLabels[type] ?? type;
878
+ options.push({ value: `__separator_${type}`, label: pc3.bold(`\u2500\u2500 ${label} ${"\u2500".repeat(Math.max(0, 40 - label.length))}`), hint: "" });
879
+ for (const item of items) {
880
+ const isInstalled = installed.has(item.name);
881
+ options.push({
882
+ value: item.name,
883
+ label: isInstalled ? pc3.dim(`${item.name} (installed)`) : item.name,
884
+ hint: item.description
885
+ });
886
+ }
887
+ }
888
+ const selected = await p2.multiselect({
889
+ message: "Select components to install:",
890
+ options
891
+ });
892
+ if (p2.isCancel(selected)) {
893
+ p2.cancel("Cancelled.");
894
+ process.exit(0);
895
+ }
896
+ components = selected.filter((s3) => !s3.startsWith("__separator_"));
897
+ if (components.length === 0) {
898
+ p2.log.warn("No components selected.");
899
+ process.exit(0);
900
+ }
799
901
  }
800
902
  let typeFilter;
801
903
  const firstAlias = resolveTypeAlias(components[0]);
@@ -853,7 +955,45 @@ async function addCommand(components, opts) {
853
955
  matches = matches.filter((m) => m.type === filteredType);
854
956
  }
855
957
  if (matches.length === 0) {
856
- newExpandedNames.push(name);
958
+ let fuzzyMatches = allIndexItems.filter(
959
+ (i) => i.name.includes(name) && (ref.namespace === "@kitn" || i.namespace === ref.namespace)
960
+ );
961
+ if (typeFilter) {
962
+ const filteredType = toComponentType(typeFilter);
963
+ fuzzyMatches = fuzzyMatches.filter((m) => m.type === filteredType);
964
+ }
965
+ if (fuzzyMatches.length === 0) {
966
+ newExpandedNames.push(name);
967
+ } else if (fuzzyMatches.length === 1) {
968
+ preResolvedTypes.set(fuzzyMatches[0].name, fuzzyMatches[0].type);
969
+ newExpandedNames.push(fuzzyMatches[0].name);
970
+ } else {
971
+ s.stop("Multiple matches found");
972
+ if (!process.stdin.isTTY) {
973
+ const suggestions = fuzzyMatches.map((m) => `${m.name} (${m.type.replace("kitn:", "")})`).join(", ");
974
+ p2.log.error(
975
+ `Component ${pc3.bold(name)} not found. Did you mean one of: ${suggestions}`
976
+ );
977
+ process.exit(1);
978
+ }
979
+ const selected = await p2.multiselect({
980
+ message: `No exact match for ${pc3.bold(name)}. Select component(s) to install:`,
981
+ options: fuzzyMatches.map((m) => ({
982
+ value: `${m.name}::${m.type}`,
983
+ label: `${m.name} ${pc3.dim(`(${m.type.replace("kitn:", "")})`)}`
984
+ }))
985
+ });
986
+ if (p2.isCancel(selected)) {
987
+ p2.cancel("Cancelled.");
988
+ process.exit(0);
989
+ }
990
+ for (const sel of selected) {
991
+ const [selName, selType] = sel.split("::");
992
+ preResolvedTypes.set(selName, selType);
993
+ newExpandedNames.push(selName);
994
+ }
995
+ s.start("Resolving dependencies...");
996
+ }
857
997
  } else if (matches.length === 1) {
858
998
  preResolvedTypes.set(name, matches[0].type);
859
999
  newExpandedNames.push(name);
@@ -913,6 +1053,68 @@ async function addCommand(components, opts) {
913
1053
  process.exit(1);
914
1054
  }
915
1055
  s.stop(`Resolved ${resolved.length} component(s)`);
1056
+ const slotReplacements = /* @__PURE__ */ new Map();
1057
+ for (const item of resolved) {
1058
+ if (!item.slot) continue;
1059
+ const existing = Object.entries(config.installed ?? {}).find(
1060
+ ([key, entry]) => key !== item.name && entry.slot === item.slot
1061
+ );
1062
+ if (!existing) continue;
1063
+ const [existingKey] = existing;
1064
+ const action = await p2.select({
1065
+ message: `${pc3.bold(existingKey)} already fills the ${pc3.cyan(item.slot)} slot. What would you like to do?`,
1066
+ options: [
1067
+ { value: "replace", label: `Replace ${existingKey} with ${item.name}` },
1068
+ { value: "add", label: `Add alongside ${existingKey}` }
1069
+ ]
1070
+ });
1071
+ if (p2.isCancel(action)) {
1072
+ p2.cancel("Cancelled.");
1073
+ process.exit(0);
1074
+ }
1075
+ if (action === "replace") {
1076
+ slotReplacements.set(existingKey, item.name);
1077
+ }
1078
+ }
1079
+ if (slotReplacements.size > 0) {
1080
+ const baseDir2 = config.aliases.base ?? "src/ai";
1081
+ for (const [oldKey] of slotReplacements) {
1082
+ const oldEntry = config.installed[oldKey];
1083
+ if (!oldEntry) continue;
1084
+ for (const filePath of oldEntry.files) {
1085
+ try {
1086
+ await unlink(join7(cwd, filePath));
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ const barrelPath2 = join7(cwd, baseDir2, "index.ts");
1091
+ const barrelEligibleDirs = /* @__PURE__ */ new Set([
1092
+ config.aliases.agents,
1093
+ config.aliases.tools,
1094
+ config.aliases.skills
1095
+ ]);
1096
+ if (existsSync(barrelPath2)) {
1097
+ let barrelContent = await readFile6(barrelPath2, "utf-8");
1098
+ let barrelChanged = false;
1099
+ for (const filePath of oldEntry.files) {
1100
+ const fileDir = dirname3(filePath);
1101
+ if (!barrelEligibleDirs.has(fileDir)) continue;
1102
+ const barrelDir2 = join7(cwd, baseDir2);
1103
+ const importPath = "./" + relative2(barrelDir2, join7(cwd, filePath)).replace(/\\/g, "/");
1104
+ const updated2 = removeImportFromBarrel(barrelContent, importPath);
1105
+ if (updated2 !== barrelContent) {
1106
+ barrelContent = updated2;
1107
+ barrelChanged = true;
1108
+ }
1109
+ }
1110
+ if (barrelChanged) {
1111
+ await writeFile6(barrelPath2, barrelContent);
1112
+ }
1113
+ }
1114
+ delete config.installed[oldKey];
1115
+ p2.log.info(`Replaced ${pc3.dim(oldKey)} \u2192 ${pc3.cyan(slotReplacements.get(oldKey))}`);
1116
+ }
1117
+ }
916
1118
  p2.log.info("Components to install:\n" + resolved.map((item) => {
917
1119
  const isExplicit = expandedNames.includes(item.name) || components.includes(item.name);
918
1120
  const label = isExplicit ? item.name : `${item.name} ${pc3.dim("(dependency)")}`;
@@ -978,10 +1180,13 @@ async function addCommand(components, opts) {
978
1180
  const installedKey = ref.namespace === "@kitn" ? item.name : `${ref.namespace}/${item.name}`;
979
1181
  installed[installedKey] = {
980
1182
  registry: ref.namespace,
1183
+ type: item.type,
1184
+ ...item.slot && { slot: item.slot },
981
1185
  version: item.version ?? "1.0.0",
982
1186
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
983
1187
  files: item.files.map((f) => join7(baseDir2, f.path)),
984
- hash: contentHash(allContent)
1188
+ hash: contentHash(allContent),
1189
+ registryDependencies: item.registryDependencies
985
1190
  };
986
1191
  config.installed = installed;
987
1192
  } else {
@@ -1047,13 +1252,16 @@ async function addCommand(components, opts) {
1047
1252
  const installedKey = ns === "@kitn" ? item.name : `${ns}/${item.name}`;
1048
1253
  installed[installedKey] = {
1049
1254
  registry: ns,
1255
+ type: item.type,
1256
+ ...item.slot && { slot: item.slot },
1050
1257
  version: item.version ?? "1.0.0",
1051
1258
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1052
1259
  files: item.files.map((f) => {
1053
1260
  const fileName = f.path.split("/").pop();
1054
1261
  return getInstallPath(config, item.type, fileName, ns);
1055
1262
  }),
1056
- hash: contentHash(allContent)
1263
+ hash: contentHash(allContent),
1264
+ registryDependencies: item.registryDependencies
1057
1265
  };
1058
1266
  config.installed = installed;
1059
1267
  }
@@ -1175,6 +1383,28 @@ import * as p3 from "@clack/prompts";
1175
1383
  import pc4 from "picocolors";
1176
1384
  import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
1177
1385
  import { join as join8 } from "path";
1386
+ function getPluginTemplate(framework) {
1387
+ const adapterName = framework === "hono-openapi" ? "hono-openapi" : framework;
1388
+ return `import { createAIPlugin } from "@kitn/adapters/${adapterName}";
1389
+ import { registerWithPlugin } from "./index.js";
1390
+
1391
+ export const ai = createAIPlugin({
1392
+ // To enable agent chat, add an AI provider:
1393
+ // https://sdk.vercel.ai/providers/ai-sdk-providers
1394
+ //
1395
+ // Example with OpenRouter (access to many models):
1396
+ // import { openrouter } from "@openrouter/ai-sdk-provider";
1397
+ // model: (id) => openrouter(id ?? "openai/gpt-4o-mini"),
1398
+ //
1399
+ // Example with OpenAI directly:
1400
+ // import { openai } from "@ai-sdk/openai";
1401
+ // model: (id) => openai(id ?? "gpt-4o-mini"),
1402
+ });
1403
+
1404
+ // Flush all auto-registered components into the plugin
1405
+ registerWithPlugin(ai);
1406
+ `;
1407
+ }
1178
1408
  async function initCommand(opts = {}) {
1179
1409
  p3.intro(pc4.bgCyan(pc4.black(" kitn init ")));
1180
1410
  const cwd = process.cwd();
@@ -1218,6 +1448,31 @@ async function initCommand(opts = {}) {
1218
1448
  }
1219
1449
  runtime = selected;
1220
1450
  }
1451
+ const validFrameworks = ["hono", "hono-openapi", "elysia"];
1452
+ let framework;
1453
+ if (opts.framework) {
1454
+ if (!validFrameworks.includes(opts.framework)) {
1455
+ p3.log.error(`Invalid framework: ${opts.framework}. Must be one of: ${validFrameworks.join(", ")}`);
1456
+ process.exit(1);
1457
+ }
1458
+ framework = opts.framework;
1459
+ } else if (opts.yes) {
1460
+ framework = "hono";
1461
+ } else {
1462
+ const selected = await p3.select({
1463
+ message: "Which HTTP framework do you use?",
1464
+ options: [
1465
+ { value: "hono", label: "Hono", hint: "recommended" },
1466
+ { value: "hono-openapi", label: "Hono + OpenAPI", hint: "zod-openapi routes with /doc endpoint" },
1467
+ { value: "elysia", label: "Elysia", hint: "Bun-native framework" }
1468
+ ]
1469
+ });
1470
+ if (p3.isCancel(selected)) {
1471
+ p3.cancel("Init cancelled.");
1472
+ process.exit(0);
1473
+ }
1474
+ framework = selected;
1475
+ }
1221
1476
  let baseDir;
1222
1477
  if (opts.base) {
1223
1478
  baseDir = opts.base;
@@ -1237,7 +1492,7 @@ async function initCommand(opts = {}) {
1237
1492
  }
1238
1493
  const config = {
1239
1494
  runtime,
1240
- framework: "hono",
1495
+ framework,
1241
1496
  aliases: {
1242
1497
  base: baseDir,
1243
1498
  agents: `${baseDir}/agents`,
@@ -1263,20 +1518,21 @@ async function initCommand(opts = {}) {
1263
1518
  ["@kitn", "@kitnai"]
1264
1519
  );
1265
1520
  p3.log.info(`Patched tsconfig.json with path: ${pc4.bold("@kitn/*")}`);
1266
- p3.log.info("Installing core engine and routes...");
1521
+ p3.log.info("Installing core engine and adapter...");
1267
1522
  await addCommand(["core", "routes"], { overwrite: true });
1268
1523
  const aiDir = join8(cwd, baseDir);
1269
1524
  await mkdir4(aiDir, { recursive: true });
1270
1525
  const barrelPath = join8(aiDir, "index.ts");
1271
1526
  await writeFile7(barrelPath, createBarrelFile());
1272
1527
  const pluginPath = join8(aiDir, "plugin.ts");
1273
- await writeFile7(pluginPath, PLUGIN_TEMPLATE);
1528
+ await writeFile7(pluginPath, getPluginTemplate(framework));
1274
1529
  p3.log.success(`Created ${pc4.bold(baseDir + "/plugin.ts")} \u2014 configure your AI provider there`);
1530
+ const mountCode = framework === "elysia" ? `app.use(ai.router);` : `app.route("/api", ai.router);`;
1275
1531
  p3.note(
1276
1532
  [
1277
1533
  `import { ai } from "./${baseDir.replace(/^src\//, "")}/plugin";`,
1278
1534
  ``,
1279
- `app.route("/api", ai.router);`
1535
+ mountCode
1280
1536
  ].join("\n"),
1281
1537
  "Add this to your server entry point:"
1282
1538
  );
@@ -1291,7 +1547,6 @@ async function initCommand(opts = {}) {
1291
1547
  );
1292
1548
  p3.outro("Done!");
1293
1549
  }
1294
- var PLUGIN_TEMPLATE;
1295
1550
  var init_init = __esm({
1296
1551
  "src/commands/init.ts"() {
1297
1552
  "use strict";
@@ -1299,25 +1554,6 @@ var init_init = __esm({
1299
1554
  init_tsconfig_patcher();
1300
1555
  init_barrel_manager();
1301
1556
  init_add();
1302
- PLUGIN_TEMPLATE = `import { createAIPlugin } from "@kitn/routes";
1303
- import { registerWithPlugin } from "./index.js";
1304
-
1305
- export const ai = createAIPlugin({
1306
- // To enable agent chat, add an AI provider:
1307
- // https://sdk.vercel.ai/providers/ai-sdk-providers
1308
- //
1309
- // Example with OpenRouter (access to many models):
1310
- // import { openrouter } from "@openrouter/ai-sdk-provider";
1311
- // model: (id) => openrouter(id ?? "openai/gpt-4o-mini"),
1312
- //
1313
- // Example with OpenAI directly:
1314
- // import { openai } from "@ai-sdk/openai";
1315
- // model: (id) => openai(id ?? "gpt-4o-mini"),
1316
- });
1317
-
1318
- // Flush all auto-registered components into the plugin
1319
- registerWithPlugin(ai);
1320
- `;
1321
1557
  }
1322
1558
  });
1323
1559
 
@@ -1542,36 +1778,16 @@ __export(remove_exports, {
1542
1778
  });
1543
1779
  import * as p6 from "@clack/prompts";
1544
1780
  import pc6 from "picocolors";
1545
- import { join as join10, relative as relative3, dirname as dirname3 } from "path";
1546
- import { unlink, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1781
+ import { join as join10, relative as relative3, dirname as dirname4 } from "path";
1782
+ import { unlink as unlink2, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1547
1783
  import { existsSync as existsSync2 } from "fs";
1548
- async function removeCommand(componentName) {
1549
- const cwd = process.cwd();
1550
- const config = await readConfig(cwd);
1551
- if (!config) {
1552
- p6.log.error("No kitn.json found. Run `kitn init` first.");
1553
- process.exit(1);
1554
- }
1555
- const input = componentName === "routes" ? resolveRoutesAlias(config) : componentName;
1556
- const ref = parseComponentRef(input);
1557
- const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1784
+ async function removeSingleComponent(installedKey, config, cwd) {
1558
1785
  const installed = config.installed?.[installedKey];
1559
- if (!installed) {
1560
- p6.log.error(`Component '${ref.name}' is not installed.`);
1561
- process.exit(1);
1562
- }
1563
- const shouldRemove = await p6.confirm({
1564
- message: `Remove ${ref.name}? This will delete ${installed.files.length} file(s).`,
1565
- initialValue: false
1566
- });
1567
- if (p6.isCancel(shouldRemove) || !shouldRemove) {
1568
- p6.cancel("Remove cancelled.");
1569
- process.exit(0);
1570
- }
1786
+ if (!installed) return;
1571
1787
  const deleted = [];
1572
1788
  for (const filePath of installed.files) {
1573
1789
  try {
1574
- await unlink(join10(cwd, filePath));
1790
+ await unlink2(join10(cwd, filePath));
1575
1791
  deleted.push(filePath);
1576
1792
  } catch {
1577
1793
  p6.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
@@ -1589,7 +1805,7 @@ async function removeCommand(componentName) {
1589
1805
  let barrelContent = await readFile7(barrelPath, "utf-8");
1590
1806
  let barrelChanged = false;
1591
1807
  for (const filePath of deleted) {
1592
- const fileDir = dirname3(filePath);
1808
+ const fileDir = dirname4(filePath);
1593
1809
  if (!barrelEligibleDirs.has(fileDir)) continue;
1594
1810
  const importPath = "./" + relative3(barrelDir, join10(cwd, filePath)).replace(/\\/g, "/");
1595
1811
  const updated = removeImportFromBarrel(barrelContent, importPath);
@@ -1604,14 +1820,113 @@ async function removeCommand(componentName) {
1604
1820
  }
1605
1821
  }
1606
1822
  delete config.installed[installedKey];
1823
+ if (deleted.length > 0) {
1824
+ p6.log.success(`Removed ${installedKey}:
1825
+ ` + deleted.map((f) => ` ${pc6.red("-")} ${f}`).join("\n"));
1826
+ }
1827
+ }
1828
+ async function offerOrphanRemoval(removedDeps, config, cwd) {
1829
+ if (removedDeps.size === 0) return;
1830
+ const remaining = Object.entries(config.installed ?? {});
1831
+ const neededDeps = /* @__PURE__ */ new Set();
1832
+ for (const [, entry] of remaining) {
1833
+ const deps = entry.registryDependencies;
1834
+ if (deps) {
1835
+ for (const dep of deps) {
1836
+ neededDeps.add(dep);
1837
+ }
1838
+ }
1839
+ }
1840
+ const orphans = [...removedDeps].filter(
1841
+ (dep) => dep !== "core" && !neededDeps.has(dep) && config.installed?.[dep]
1842
+ );
1843
+ if (orphans.length === 0) return;
1844
+ const selected = await p6.multiselect({
1845
+ message: "The following dependencies are no longer used. Remove them?",
1846
+ options: orphans.map((dep) => ({
1847
+ value: dep,
1848
+ label: dep,
1849
+ hint: `${config.installed[dep].files.length} file(s)`
1850
+ })),
1851
+ initialValues: orphans
1852
+ // all checked by default
1853
+ });
1854
+ if (p6.isCancel(selected)) return;
1855
+ for (const key of selected) {
1856
+ await removeSingleComponent(key, config, cwd);
1857
+ }
1858
+ }
1859
+ async function removeCommand(componentName) {
1860
+ const cwd = process.cwd();
1861
+ const config = await readConfig(cwd);
1862
+ if (!config) {
1863
+ p6.log.error("No kitn.json found. Run `kitn init` first.");
1864
+ process.exit(1);
1865
+ }
1866
+ if (!componentName) {
1867
+ const installed2 = config.installed ?? {};
1868
+ const installedKeys = Object.keys(installed2);
1869
+ if (installedKeys.length === 0) {
1870
+ p6.log.warn("No components installed.");
1871
+ process.exit(0);
1872
+ }
1873
+ const selected = await p6.multiselect({
1874
+ message: "Select components to remove:",
1875
+ options: installedKeys.map((key) => ({
1876
+ value: key,
1877
+ label: key,
1878
+ hint: `${installed2[key].files.length} file(s)`
1879
+ }))
1880
+ });
1881
+ if (p6.isCancel(selected)) {
1882
+ p6.cancel("Cancelled.");
1883
+ process.exit(0);
1884
+ }
1885
+ const selectedKeys = selected;
1886
+ if (selectedKeys.length === 0) {
1887
+ p6.log.warn("No components selected.");
1888
+ process.exit(0);
1889
+ }
1890
+ for (const key of selectedKeys) {
1891
+ await removeSingleComponent(key, config, cwd);
1892
+ }
1893
+ const allRemovedDeps = /* @__PURE__ */ new Set();
1894
+ for (const key of selectedKeys) {
1895
+ const entry = installed2[key];
1896
+ if (entry?.registryDependencies) {
1897
+ for (const dep of entry.registryDependencies) {
1898
+ allRemovedDeps.add(dep);
1899
+ }
1900
+ }
1901
+ }
1902
+ await offerOrphanRemoval(allRemovedDeps, config, cwd);
1903
+ await writeConfig(cwd, config);
1904
+ p6.outro(pc6.green("Done!"));
1905
+ return;
1906
+ }
1907
+ const input = componentName === "routes" ? resolveRoutesAlias(config) : componentName;
1908
+ const ref = parseComponentRef(input);
1909
+ const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1910
+ const installed = config.installed?.[installedKey];
1911
+ if (!installed) {
1912
+ p6.log.error(`Component '${ref.name}' is not installed.`);
1913
+ process.exit(1);
1914
+ }
1915
+ const shouldRemove = await p6.confirm({
1916
+ message: `Remove ${ref.name}? This will delete ${installed.files.length} file(s).`,
1917
+ initialValue: false
1918
+ });
1919
+ if (p6.isCancel(shouldRemove) || !shouldRemove) {
1920
+ p6.cancel("Remove cancelled.");
1921
+ process.exit(0);
1922
+ }
1923
+ await removeSingleComponent(installedKey, config, cwd);
1924
+ const removedDeps = new Set(installed.registryDependencies ?? []);
1925
+ await offerOrphanRemoval(removedDeps, config, cwd);
1607
1926
  if (Object.keys(config.installed).length === 0) {
1608
1927
  delete config.installed;
1609
1928
  }
1610
1929
  await writeConfig(cwd, config);
1611
- if (deleted.length > 0) {
1612
- p6.log.success(`Removed ${ref.name}:
1613
- ` + deleted.map((f) => ` ${pc6.red("-")} ${f}`).join("\n"));
1614
- }
1615
1930
  }
1616
1931
  var init_remove = __esm({
1617
1932
  "src/commands/remove.ts"() {
@@ -1946,6 +2261,7 @@ var check_exports = {};
1946
2261
  __export(check_exports, {
1947
2262
  checkCommand: () => checkCommand
1948
2263
  });
2264
+ import { execSync as execSync2 } from "child_process";
1949
2265
  import * as p10 from "@clack/prompts";
1950
2266
  import pc9 from "picocolors";
1951
2267
  async function checkCommand(currentVersion) {
@@ -1961,7 +2277,22 @@ async function checkCommand(currentVersion) {
1961
2277
  }
1962
2278
  if (isNewer(latest, currentVersion)) {
1963
2279
  s.stop(pc9.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
1964
- p10.log.message(` Run: ${pc9.cyan("npm i -g @kitnai/cli")}`);
2280
+ const pm = detectCliInstaller();
2281
+ const installCmd = getGlobalInstallCommand(pm, "@kitnai/cli");
2282
+ const shouldUpdate = await p10.confirm({ message: "Update now?" });
2283
+ if (p10.isCancel(shouldUpdate) || !shouldUpdate) {
2284
+ p10.log.message(` Run: ${pc9.cyan(installCmd)}`);
2285
+ } else {
2286
+ const us = p10.spinner();
2287
+ us.start("Updating...");
2288
+ try {
2289
+ execSync2(installCmd, { stdio: "pipe" });
2290
+ us.stop(pc9.green(`Updated to v${latest}`));
2291
+ } catch {
2292
+ us.stop(pc9.red("Update failed"));
2293
+ p10.log.message(` Run manually: ${pc9.cyan(installCmd)}`);
2294
+ }
2295
+ }
1965
2296
  } else {
1966
2297
  s.stop(pc9.green("You're on the latest version"));
1967
2298
  }
@@ -1970,6 +2301,7 @@ async function checkCommand(currentVersion) {
1970
2301
  var init_check = __esm({
1971
2302
  "src/commands/check.ts"() {
1972
2303
  "use strict";
2304
+ init_detect();
1973
2305
  init_update_check();
1974
2306
  }
1975
2307
  });
@@ -2073,14 +2405,14 @@ var init_registry = __esm({
2073
2405
  // src/index.ts
2074
2406
  init_update_check();
2075
2407
  import { Command } from "commander";
2076
- var VERSION = true ? "0.1.22" : "0.0.0-dev";
2408
+ var VERSION = true ? "0.1.24" : "0.0.0-dev";
2077
2409
  var printUpdateNotice = startUpdateCheck(VERSION);
2078
2410
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
2079
- program.command("init").description("Initialize kitn in your project").option("-r, --runtime <runtime>", "runtime to use (bun, node, deno)").option("-b, --base <path>", "base directory for components (default: src/ai)").option("-y, --yes", "accept all defaults without prompting").action(async (opts) => {
2411
+ 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) => {
2080
2412
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2081
2413
  await initCommand2(opts);
2082
2414
  });
2083
- program.command("add").description("Add components from the registry (supports type-first: kitn add agent <name>)").argument("[components...]", "component names or type followed by names").option("-o, --overwrite", "overwrite existing files without prompting").option("-t, --type <type>", "filter by component type during resolution").action(async (components, opts) => {
2415
+ program.command("add").alias("install").description("Add components from the registry (supports type-first: kitn add agent <name>)").argument("[components...]", "component names or type followed by names").option("-o, --overwrite", "overwrite existing files without prompting").option("-t, --type <type>", "filter by component type during resolution").action(async (components, opts) => {
2084
2416
  const { addCommand: addCommand2 } = await Promise.resolve().then(() => (init_add(), add_exports));
2085
2417
  await addCommand2(components, opts);
2086
2418
  });
@@ -2092,7 +2424,7 @@ program.command("diff").description("Show differences between local and registry
2092
2424
  const { diffCommand: diffCommand2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
2093
2425
  await diffCommand2(component);
2094
2426
  });
2095
- program.command("remove").description("Remove an installed component").argument("<component>", "component name to remove").action(async (component) => {
2427
+ program.command("remove").alias("uninstall").description("Remove an installed component").argument("[component]", "component name to remove (interactive if omitted)").action(async (component) => {
2096
2428
  const { removeCommand: removeCommand2 } = await Promise.resolve().then(() => (init_remove(), remove_exports));
2097
2429
  await removeCommand2(component);
2098
2430
  });