kooni-bot 0.2.5 → 0.2.8

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.
Files changed (2) hide show
  1. package/bin/kooni.js +177 -44
  2. package/package.json +1 -1
package/bin/kooni.js CHANGED
@@ -22,7 +22,7 @@ import { execFileSync, spawnSync } from "node:child_process";
22
22
  import { randomUUID } from "node:crypto";
23
23
  import { fileURLToPath } from "node:url";
24
24
 
25
- const CLI_VERSION = "0.2.5";
25
+ const CLI_VERSION = "0.2.8";
26
26
 
27
27
  const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
28
28
  const BRANCH = process.env.KOONI_BRANCH || "main";
@@ -33,6 +33,7 @@ const CHECKIN_URL = process.env.KOONI_CHECKIN_URL || "https://f5gacw7g.function2
33
33
 
34
34
  const CFG_DIR = join(homedir(), ".kooni");
35
35
  const CFG_FILE = join(CFG_DIR, "config.json");
36
+ const INSTALLS_FILE = join(CFG_DIR, "installs.json");
36
37
  const MARKER = ".kooni-bot.json";
37
38
  const SKILL_DIR = join(homedir(), ".claude", "skills", "kooni");
38
39
 
@@ -250,6 +251,26 @@ function banner() {
250
251
  function loadCfg() { try { return JSON.parse(readFileSync(CFG_FILE, "utf8")); } catch { return {}; } }
251
252
  function saveCfg(o) { mkdirSync(CFG_DIR, { recursive: true }); writeFileSync(CFG_FILE, JSON.stringify(o, null, 2)); }
252
253
 
254
+ // Registro local de instalaciones (~/.kooni/installs.json): cada instalación de
255
+ // esta computadora queda identificada por su carpeta real + uid de Cloudflare.
256
+ // Sirve para que `deploy`/`update`/`doctor` sepan cuál elegir si hay varias.
257
+ function loadInstalls() {
258
+ try { return JSON.parse(readFileSync(INSTALLS_FILE, "utf8")); } catch { return []; }
259
+ }
260
+ function saveInstalls(list) {
261
+ mkdirSync(CFG_DIR, { recursive: true });
262
+ writeFileSync(INSTALLS_FILE, JSON.stringify(list, null, 2));
263
+ }
264
+ function recordInstall(dir, meta) {
265
+ const real = realpathSync(dir);
266
+ const list = loadInstalls().filter((x) => x && x.dir !== real);
267
+ list.push({ dir: real, ...meta, updatedAt: new Date().toISOString() });
268
+ saveInstalls(list);
269
+ }
270
+ function listInstalls() {
271
+ return loadInstalls().filter((x) => x && x.dir && existsSync(x.dir));
272
+ }
273
+
253
274
  // ── flags / interacción ──────────────────────────────────────────────────────
254
275
  function parseFlags(args) {
255
276
  const flags = {};
@@ -461,10 +482,9 @@ function backupBeforeUpdate(dir, version) {
461
482
  }
462
483
 
463
484
  // ── markers / detección ──────────────────────────────────────────────────────
464
- function writeMarker(dir, { slug, version, lang }) {
465
- writeFileSync(join(dir, MARKER), JSON.stringify({
466
- slug, version, lang, updatedAt: new Date().toISOString(),
467
- }, null, 2));
485
+ function writeMarker(dir, meta) {
486
+ const prev = readMarker(dir) || {};
487
+ writeFileSync(join(dir, MARKER), JSON.stringify({ ...prev, ...meta, updatedAt: new Date().toISOString() }, null, 2));
468
488
  }
469
489
 
470
490
  function readMarker(dir) {
@@ -482,9 +502,28 @@ function isKooni(dir) {
482
502
  return existsSync(join(dir, "package.json")) && (existsSync(join(dir, "member")) || existsSync(join(dir, "src", "index.ts")));
483
503
  }
484
504
 
485
- function resolveBotDir(arg) {
505
+ // Elige la instalación sobre la que actuar. `arg` es una ruta explícita (gana).
506
+ // Si no hay ruta, prioriza el cwd; después el registro local `installs.json`;
507
+ // y como último recurso escanea los hijos del cwd. Devuelve la ruta o null.
508
+ async function resolveBotDir(arg, rl) {
486
509
  if (arg && isKooni(arg)) return arg;
487
510
  if (isKooni(process.cwd())) return process.cwd();
511
+
512
+ const registered = listInstalls();
513
+ if (registered.length === 1) return registered[0].dir;
514
+ if (registered.length > 1) {
515
+ if (!interactive()) {
516
+ console.log(C.yellow("\n " + m("Hay varias instalaciones de Kooni. Pasa la carpeta explícita:", "Multiple Kooni installs found. Pass an explicit folder:")));
517
+ registered.forEach((x) => console.log(" " + C.cyan(`npx kooni-bot <comando> "${x.dir}"`)));
518
+ process.exit(1);
519
+ }
520
+ const idx = await select(rl, m("¿Cuál instalación?", "Which install?"), registered.map((x) => ({
521
+ key: x.dir,
522
+ label: `${x.slug || basename(x.dir)} · ${x.dir}`,
523
+ })));
524
+ return registered[idx]?.dir || null;
525
+ }
526
+
488
527
  for (const e of readdirSync(process.cwd())) {
489
528
  try {
490
529
  const p = join(process.cwd(), e);
@@ -559,7 +598,7 @@ function sanitizeSlug(s) {
559
598
  return String(s || "mi-negocio").toLowerCase().replace(/ /g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "mi-negocio";
560
599
  }
561
600
 
562
- function stampWrangler(dir, answers) {
601
+ function stampWrangler(dir, answers, botUid) {
563
602
  const wt = join(dir, "wrangler.toml");
564
603
  const example = join(dir, "wrangler.toml.example");
565
604
  // El template distribuye wrangler.toml.example; init genera el wrangler.toml real.
@@ -569,15 +608,21 @@ function stampWrangler(dir, answers) {
569
608
  if (!existsSync(wt)) return null;
570
609
  let s = readFileSync(wt, "utf8");
571
610
  const slug = answers.slug;
572
- // Nombres canónicos del proyecto (coinciden con `pnpm db:apply:remote`, que
573
- // apunta a `kooni_db`, y con la instalación probada del template).
574
- const dbName = "kooni_db";
575
- const kbName = "kooni_kb";
611
+ // Identidad ÚNICA por instalación: cada bot genera su propio uid de 6 chars y
612
+ // lo usa en worker/D1/Vectorize. Así dos instalaciones en la MISMA cuenta de
613
+ // Cloudflare nunca comparten datos. Si el wrangler.toml ya trae un uid (reinstall
614
+ // en la misma carpeta), se reutiliza.
615
+ const existingUid = (s.match(/name\s*=\s*"kooni-bot-.+-([a-f0-9]{6})"/) || [])[1];
616
+ const uid = existingUid || botUid || randomUUID().replace(/-/g, "").slice(0, 6);
617
+ const resId = slug.replace(/-/g, "_");
618
+ const dbName = `kooni_${resId}_${uid}_db`;
619
+ const kbName = `kooni_${resId}_${uid}_kb`;
620
+ const workerName = `kooni-bot-${slug}-${uid}`;
576
621
  const R = REGIONS[answers.lang] || REGIONS["es-MX"];
577
622
 
578
623
  const set = (re, val) => { s = s.replace(re, val); };
579
624
 
580
- set(/^name\s*=\s*"[^"]*"/m, `name = "kooni-bot-${slug}"`);
625
+ set(/^name\s*=\s*"[^"]*"/m, `name = "${workerName}"`);
581
626
  set(/BOT_NAME\s*=\s*"[^"]*"/g, `BOT_NAME = "${String(answers.botName).replace(/"/g, "'")}"`);
582
627
  set(/BUSINESS_NAME\s*=\s*"[^"]*"/g, `BUSINESS_NAME = "${String(answers.businessName).replace(/"/g, "'")}"`);
583
628
  set(/BOT_LANGUAGE\s*=\s*"[^"]*"/g, `BOT_LANGUAGE = "${answers.lang}"`);
@@ -597,6 +642,13 @@ function stampWrangler(dir, answers) {
597
642
  return new RegExp(`^\\s*${key}\\s*=`, "m").test(varsMatch[0]);
598
643
  };
599
644
 
645
+ // Identidad de instalación para ligar licencias (ver src/license.ts + limits.ts).
646
+ if (hasInVars("BOT_INSTANCE_ID")) {
647
+ set(/BOT_INSTANCE_ID\s*=\s*"[^"]*"/g, `BOT_INSTANCE_ID = "${uid}"`);
648
+ } else {
649
+ s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1BOT_INSTANCE_ID = "${uid}"\n`);
650
+ }
651
+
600
652
  if (answers.provider !== "anthropic") {
601
653
  if (hasInVars("LLM_PROVIDER")) {
602
654
  set(/LLM_PROVIDER\s*=\s*"[^"]*"/g, `LLM_PROVIDER = "${answers.provider}"`);
@@ -617,7 +669,7 @@ function stampWrangler(dir, answers) {
617
669
  }
618
670
 
619
671
  writeFileSync(wt, s);
620
- return { dbName, kbName, tz: R.tz, currency: R.currency };
672
+ return { uid, dbName, kbName, workerName, tz: R.tz, currency: R.currency };
621
673
  }
622
674
 
623
675
  function renderMemberConfig(answers, meta) {
@@ -696,6 +748,7 @@ function collectAnswers(flags) {
696
748
  // `onboarding()` lo pregunte (interactivo) o use el default (no-interactivo).
697
749
  return {
698
750
  slug: flags.slug ? sanitizeSlug(flags.slug) : undefined,
751
+ uid: String(flags.uid || "").trim().toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 6) || undefined,
699
752
  businessName: String(flags.negocio || flags.nombre || flags.name || "").trim() || undefined,
700
753
  botName: String(flags["bot-name"] || "").trim() || undefined,
701
754
  lang: flags.lang ? normBotLang(flags.lang) : undefined,
@@ -821,10 +874,11 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
821
874
  wrangler(dir, ["login"]);
822
875
  console.log(" " + C.green("✓") + " " + t().loginOk);
823
876
 
824
- // recursos (nombres canónicos del proyecto: kooni_db / kooni_kb)
877
+ // recursos (nombres ÚNICOS por instalación, ya estampados en wrangler.toml)
825
878
  console.log("\n " + C.dim(t().creatingResources));
826
- const dbName = "kooni_db";
827
- const kbName = "kooni_kb";
879
+ const wtRaw = readFileSync(wt, "utf8");
880
+ const dbName = (wtRaw.match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
881
+ const kbName = (wtRaw.match(/index_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_kb";
828
882
 
829
883
  let d1Id = "";
830
884
  try {
@@ -904,7 +958,7 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
904
958
  console.log(" " + C.green("✓") + " " + m("dependencias listas", "dependencies ready"));
905
959
 
906
960
  console.log(" " + C.dim(t().migrations));
907
- runPnpm(dir, ["db:apply:remote"]);
961
+ wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]);
908
962
  console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
909
963
 
910
964
  console.log(" " + C.dim(t().deploying));
@@ -925,6 +979,8 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
925
979
  } else {
926
980
  console.log(C.yellow(" ⚠ " + m("no se detectó la URL del worker — revisa la salida del deploy", "couldn't detect the worker URL — check the deploy output")));
927
981
  }
982
+ // Persistir identidad de Cloudflare en el marker (para update/doctor/selector).
983
+ writeMarker(dir, { databaseId: d1Id, workerUrl: url || undefined, dbName, kbName });
928
984
  return url;
929
985
  }
930
986
 
@@ -932,14 +988,19 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
932
988
  async function checkin(dir, answers, version) {
933
989
  if (process.env.KOONI_NO_CHECKIN === "1" || process.env.KOONI_SILENT === "1") return;
934
990
  try {
935
- const slug = answers.slug || basename(dir);
991
+ const marker = readMarker(dir) || {};
992
+ const slug = answers.slug || marker.slug || basename(dir);
936
993
  await fetchTimeout(CHECKIN_URL, {
937
994
  method: "POST",
938
995
  headers: { "Content-Type": "application/json" },
939
996
  body: JSON.stringify({
940
997
  email: answers.email || undefined,
941
998
  slug,
942
- workerUrl: `https://kooni-bot-${slug}.workers.dev`,
999
+ uid: marker.uid,
1000
+ workerName: marker.workerName,
1001
+ dbName: marker.dbName,
1002
+ kbName: marker.kbName,
1003
+ workerUrl: marker.workerUrl || `https://kooni-bot-${slug}.workers.dev`,
943
1004
  cliVersion: CLI_VERSION,
944
1005
  botVersion: version,
945
1006
  tier: answers.tier,
@@ -973,9 +1034,14 @@ constructor. No hay licencia ni servidor de Horizontes: el tier free/pro se cont
973
1034
  - \`npx kooni-bot init [dir]\` — descarga el template, configura (idioma, negocio, cerebro) y ofrece desplegar.
974
1035
  - \`npx kooni-bot deploy [dir]\` — provisiona Cloudflare (login, D1/Vectorize/R2, secrets, migraciones, deploy).
975
1036
  - \`npx kooni-bot update [dir]\` — trae la versión nueva conservando \`member/\`, \`wrangler.toml\` y datos.
1037
+ - \`npx kooni-bot update --all\` — actualiza TODAS las instalaciones registradas en esta computadora.
976
1038
  - \`npx kooni-bot doctor [dir]\` — diagnostica el bot instalado.
977
1039
  - \`npx kooni-bot version\`.
978
1040
 
1041
+ ## Conexión de canales (DESDE el panel, sin redeploy)
1042
+ - Telegram y Zernio se conectan pegando su token/API key en \`/admin/conexiones\`. Se guardan en D1 (\`settings\`) y el canal se pone verde al instante, SIN \`wrangler secret put\` ni redeploy.
1043
+ - La URL del webhook de cada canal se muestra en su propia card (con botón copiar).
1044
+
979
1045
  ## Regla de oro (memorízala)
980
1046
  | Carpeta / archivo | Qué pasa al actualizar |
981
1047
  |---|---|
@@ -1079,7 +1145,7 @@ async function cmdInit(flags, rest) {
1079
1145
  const answers = collectAnswers(flags);
1080
1146
  await onboarding(rl, answers, basename(dir));
1081
1147
 
1082
- const meta = stampWrangler(dir, answers);
1148
+ const meta = stampWrangler(dir, answers, answers.uid);
1083
1149
  const kbToken = "kooni-reindex-" + randomUUID().replace(/-/g, "").slice(0, 12);
1084
1150
  if (existsSync(join(dir, "member"))) {
1085
1151
  const region = REGIONS[answers.lang] || REGIONS["es-MX"];
@@ -1090,9 +1156,26 @@ async function cmdInit(flags, rest) {
1090
1156
  }));
1091
1157
  }
1092
1158
  writeDevVars(dir, answers, kbToken);
1093
- writeMarker(dir, { slug: answers.slug, version, lang: L });
1159
+ writeMarker(dir, {
1160
+ slug: answers.slug,
1161
+ version,
1162
+ lang: L,
1163
+ uid: (meta && meta.uid) || answers.uid,
1164
+ workerName: meta && meta.workerName,
1165
+ dbName: meta && meta.dbName,
1166
+ kbName: meta && meta.kbName,
1167
+ });
1094
1168
  console.log("\n " + C.green("✓") + " " + t().configDone);
1095
1169
 
1170
+ // Registrar la instalación local para el selector multi-bot.
1171
+ recordInstall(dir, {
1172
+ slug: answers.slug,
1173
+ uid: (meta && meta.uid) || answers.uid,
1174
+ workerName: meta && meta.workerName,
1175
+ dbName: meta && meta.dbName,
1176
+ kbName: meta && meta.kbName,
1177
+ });
1178
+
1096
1179
  // deploy (si no lo deshabilitan)
1097
1180
  if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
1098
1181
  const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
@@ -1122,11 +1205,11 @@ async function cmdDeploy(flags, rest) {
1122
1205
  banner();
1123
1206
  installAgentSkill(flags);
1124
1207
 
1125
- const dir = resolveBotDir(rest[0]);
1126
- if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1127
-
1128
1208
  const rl = createInterface({ input, output });
1129
1209
  try {
1210
+ const dir = await resolveBotDir(rest[0], rl);
1211
+ if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1212
+
1130
1213
  // provider para elegir el secret correcto
1131
1214
  let brainKey = "claude";
1132
1215
  try {
@@ -1144,24 +1227,16 @@ async function cmdDeploy(flags, rest) {
1144
1227
  }
1145
1228
  }
1146
1229
 
1147
- async function cmdUpdate(flags, rest) {
1148
- const cfg = loadCfg();
1149
- ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
1150
- if (flags.lang === "en" || cfg.lang === "en") L = "en";
1151
- banner();
1152
- installAgentSkill(flags);
1153
-
1154
- const dir = resolveBotDir(rest[0]);
1155
- if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1156
-
1230
+ // Actualiza UNA instalación. Devuelve un resumen; NO hace process.exit (para que
1231
+ // `--all` pueda recorrer varias sin cortar el proceso).
1232
+ async function updateOne(dir, flags = {}) {
1157
1233
  const marker = readMarker(dir) || {};
1234
+ const slug = marker.slug || basename(dir);
1158
1235
  const current = marker.version || readPkgVersion(dir) || "0.0.0";
1159
1236
 
1160
1237
  const tgz = join(dir, ".kooni-template.tgz");
1161
- process.stdout.write(C.dim(" " + t().updRevalidating + "\n"));
1162
1238
  await downloadTemplate(tgz);
1163
1239
 
1164
- // versión nueva desde el tarball
1165
1240
  const tmp = join(dir, ".kooni-extract");
1166
1241
  mkdirSync(tmp, { recursive: true });
1167
1242
  const src = extractToTemp(tgz, tmp);
@@ -1176,29 +1251,26 @@ async function cmdUpdate(flags, rest) {
1176
1251
  if (!verLt(current, next)) {
1177
1252
  rmSync(tmp, { recursive: true, force: true });
1178
1253
  rmSync(tgz, { force: true });
1179
- console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + current + ")\n");
1180
- return;
1254
+ return { dir, slug, updated: false, from: current, to: next };
1181
1255
  }
1182
1256
 
1183
- // respaldo + extraer sobre la instalación
1184
1257
  const backupPath = backupBeforeUpdate(dir, current);
1185
1258
  extractOver(tgz, dir);
1186
- writeMarker(dir, { slug: marker.slug || basename(dir), version: next, lang: marker.lang || L });
1259
+ writeMarker(dir, { slug, version: next, lang: marker.lang || L });
1187
1260
 
1188
1261
  console.log(" " + C.green("✓") + " " + t().updDone(next));
1189
1262
  if (backupPath) console.log(" " + C.dim(t().updBackup(backupPath.slice(dir.length + 1))));
1190
1263
  console.log(" " + C.dim(t().updPreserved));
1191
1264
  console.log(" " + C.dim(t().updReplaced));
1192
1265
 
1193
- // dependencias + migraciones + reindex + deploy
1194
1266
  console.log("\n " + C.dim(t().installing));
1195
1267
  runPnpm(dir, ["install"]);
1196
- try { runPnpm(dir, ["db:apply:remote"]); } catch { /* best-effort */ }
1268
+ const dbName = marker.dbName || (readFileSync(join(dir, "wrangler.toml"), "utf8").match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
1269
+ try { wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]); } catch { /* best-effort */ }
1197
1270
  try { runPnpm(dir, ["kb:reindex"]); } catch { /* best-effort */ }
1198
1271
  console.log(" " + C.dim(t().deploying));
1199
1272
  try { runPnpm(dir, ["run", "deploy"]); } catch { /* el deploy-check imprime el detalle */ }
1200
1273
 
1201
- // reindex del worker si hay estado con URL
1202
1274
  try {
1203
1275
  const st = JSON.parse(readFileSync(join(dir, ".bot-state.json"), "utf8"));
1204
1276
  if (st.worker_url) {
@@ -1208,6 +1280,60 @@ async function cmdUpdate(flags, rest) {
1208
1280
  }
1209
1281
  } catch {}
1210
1282
 
1283
+ return { dir, slug, updated: true, from: current, to: next };
1284
+ }
1285
+
1286
+ async function cmdUpdate(flags, rest) {
1287
+ const cfg = loadCfg();
1288
+ ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
1289
+ if (flags.lang === "en" || cfg.lang === "en") L = "en";
1290
+ banner();
1291
+ installAgentSkill(flags);
1292
+
1293
+ // --all: actualiza todas las instalaciones registradas en ~/.kooni/installs.json.
1294
+ if (flags.all) {
1295
+ const dirs = listInstalls().map((x) => x.dir);
1296
+ if (dirs.length === 0) {
1297
+ console.log(" " + C.red(t().needDir) + " (sin instalaciones registradas)\n");
1298
+ process.exit(1);
1299
+ }
1300
+ console.log(" " + C.dim(m(`Actualizando ${dirs.length} instalaciones…`, `Updating ${dirs.length} installs…`)) + "\n");
1301
+ const results = [];
1302
+ for (const d of dirs) {
1303
+ const marker = readMarker(d) || {};
1304
+ const slug = marker.slug || basename(d);
1305
+ console.log(C.b("\n ◇ " + slug));
1306
+ try {
1307
+ const r = await updateOne(d, flags);
1308
+ results.push(r);
1309
+ console.log(r.updated
1310
+ ? " " + C.green(`✓ ${slug}: ${r.from} → ${r.to}`)
1311
+ : " " + C.green(`✓ ${slug}: ${t().updUpToDate} (v${r.to})`));
1312
+ } catch (e) {
1313
+ results.push({ dir: d, slug, error: e.message || String(e) });
1314
+ console.log(" " + C.red(`✗ ${slug}: ${e.message || e}`));
1315
+ }
1316
+ }
1317
+ console.log("");
1318
+ return;
1319
+ }
1320
+
1321
+ const rl = createInterface({ input, output });
1322
+ let dir;
1323
+ try {
1324
+ dir = await resolveBotDir(rest[0], rl);
1325
+ } finally {
1326
+ rl.close();
1327
+ }
1328
+ if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1329
+
1330
+ const marker = readMarker(dir) || {};
1331
+ const slug = marker.slug || basename(dir);
1332
+ console.log(C.b("\n ◇ " + slug));
1333
+ const r = await updateOne(dir, flags);
1334
+ if (!r.updated) {
1335
+ console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + r.to + ")\n");
1336
+ }
1211
1337
  console.log("");
1212
1338
  }
1213
1339
 
@@ -1220,7 +1346,13 @@ async function cmdDoctor(flags, rest) {
1220
1346
  const warn = (s, h) => { console.log(" " + C.yellow("⚠") + " " + s); if (h) console.log(" " + C.dim(h)); };
1221
1347
  const bad = (s, h) => { console.log(" " + C.red("✗") + " " + s); if (h) console.log(" " + C.dim(h)); };
1222
1348
 
1223
- const dir = resolveBotDir(rest[0]);
1349
+ const rl = createInterface({ input, output });
1350
+ let dir;
1351
+ try {
1352
+ dir = await resolveBotDir(rest[0], rl);
1353
+ } finally {
1354
+ rl.close();
1355
+ }
1224
1356
  if (!dir) { bad(t().needDir + " " + (rest[0] || process.cwd())); process.exit(1); }
1225
1357
  ok(m("Bot encontrado en ", "Bot found in ") + C.cyan(dir));
1226
1358
 
@@ -1279,6 +1411,7 @@ ${C.cyan("kooni-bot")} — ${t().helpIntro}
1279
1411
  ${C.cyan("npx kooni-bot init [dir]")} ${m("instala (descarga template + config + deploy)", "install (download template + config + deploy)")}
1280
1412
  ${C.cyan("npx kooni-bot deploy [dir]")} ${m("provisiona Cloudflare y publica el worker", "provision Cloudflare and publish the worker")}
1281
1413
  ${C.cyan("npx kooni-bot update [dir]")} ${m("actualiza sin perder tu configuración", "update without losing config")}
1414
+ ${C.cyan("npx kooni-bot update --all")} ${m("actualiza TODAS las instalaciones registradas", "update ALL registered installs")}
1282
1415
  ${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
1283
1416
  ${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
1284
1417
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kooni-bot",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "description": "Kooni — instala tu asistente de IA multicanal (WhatsApp, Instagram, Messenger, Telegram) en TU Cloudflare, en un comando.",
5
5
  "license": "MIT",
6
6
  "type": "module",