kooni-bot 0.2.5 → 0.2.9

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 +195 -51
  2. package/package.json +24 -24
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.9";
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
 
@@ -219,6 +220,7 @@ const BRAINS = {
219
220
  claude: { provider: "anthropic", secret: "ANTHROPIC_API_KEY", label: "Claude" },
220
221
  chatgpt: { provider: "openai", secret: "OPENAI_API_KEY", label: "ChatGPT" },
221
222
  grok: { provider: "xai", secret: "XAI_API_KEY", label: "Grok" },
223
+ minimax: { provider: "minimax", secret: "MINIMAX_API_KEY", label: "MiniMax" },
222
224
  gateway: { provider: "openai", secret: "OPENAI_API_KEY", label: "Gateway" },
223
225
  };
224
226
 
@@ -250,6 +252,26 @@ function banner() {
250
252
  function loadCfg() { try { return JSON.parse(readFileSync(CFG_FILE, "utf8")); } catch { return {}; } }
251
253
  function saveCfg(o) { mkdirSync(CFG_DIR, { recursive: true }); writeFileSync(CFG_FILE, JSON.stringify(o, null, 2)); }
252
254
 
255
+ // Registro local de instalaciones (~/.kooni/installs.json): cada instalación de
256
+ // esta computadora queda identificada por su carpeta real + uid de Cloudflare.
257
+ // Sirve para que `deploy`/`update`/`doctor` sepan cuál elegir si hay varias.
258
+ function loadInstalls() {
259
+ try { return JSON.parse(readFileSync(INSTALLS_FILE, "utf8")); } catch { return []; }
260
+ }
261
+ function saveInstalls(list) {
262
+ mkdirSync(CFG_DIR, { recursive: true });
263
+ writeFileSync(INSTALLS_FILE, JSON.stringify(list, null, 2));
264
+ }
265
+ function recordInstall(dir, meta) {
266
+ const real = realpathSync(dir);
267
+ const list = loadInstalls().filter((x) => x && x.dir !== real);
268
+ list.push({ dir: real, ...meta, updatedAt: new Date().toISOString() });
269
+ saveInstalls(list);
270
+ }
271
+ function listInstalls() {
272
+ return loadInstalls().filter((x) => x && x.dir && existsSync(x.dir));
273
+ }
274
+
253
275
  // ── flags / interacción ──────────────────────────────────────────────────────
254
276
  function parseFlags(args) {
255
277
  const flags = {};
@@ -461,10 +483,9 @@ function backupBeforeUpdate(dir, version) {
461
483
  }
462
484
 
463
485
  // ── 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));
486
+ function writeMarker(dir, meta) {
487
+ const prev = readMarker(dir) || {};
488
+ writeFileSync(join(dir, MARKER), JSON.stringify({ ...prev, ...meta, updatedAt: new Date().toISOString() }, null, 2));
468
489
  }
469
490
 
470
491
  function readMarker(dir) {
@@ -482,9 +503,28 @@ function isKooni(dir) {
482
503
  return existsSync(join(dir, "package.json")) && (existsSync(join(dir, "member")) || existsSync(join(dir, "src", "index.ts")));
483
504
  }
484
505
 
485
- function resolveBotDir(arg) {
506
+ // Elige la instalación sobre la que actuar. `arg` es una ruta explícita (gana).
507
+ // Si no hay ruta, prioriza el cwd; después el registro local `installs.json`;
508
+ // y como último recurso escanea los hijos del cwd. Devuelve la ruta o null.
509
+ async function resolveBotDir(arg, rl) {
486
510
  if (arg && isKooni(arg)) return arg;
487
511
  if (isKooni(process.cwd())) return process.cwd();
512
+
513
+ const registered = listInstalls();
514
+ if (registered.length === 1) return registered[0].dir;
515
+ if (registered.length > 1) {
516
+ if (!interactive()) {
517
+ console.log(C.yellow("\n " + m("Hay varias instalaciones de Kooni. Pasa la carpeta explícita:", "Multiple Kooni installs found. Pass an explicit folder:")));
518
+ registered.forEach((x) => console.log(" " + C.cyan(`npx kooni-bot <comando> "${x.dir}"`)));
519
+ process.exit(1);
520
+ }
521
+ const idx = await select(rl, m("¿Cuál instalación?", "Which install?"), registered.map((x) => ({
522
+ key: x.dir,
523
+ label: `${x.slug || basename(x.dir)} · ${x.dir}`,
524
+ })));
525
+ return registered[idx]?.dir || null;
526
+ }
527
+
488
528
  for (const e of readdirSync(process.cwd())) {
489
529
  try {
490
530
  const p = join(process.cwd(), e);
@@ -559,7 +599,7 @@ function sanitizeSlug(s) {
559
599
  return String(s || "mi-negocio").toLowerCase().replace(/ /g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "mi-negocio";
560
600
  }
561
601
 
562
- function stampWrangler(dir, answers) {
602
+ function stampWrangler(dir, answers, botUid) {
563
603
  const wt = join(dir, "wrangler.toml");
564
604
  const example = join(dir, "wrangler.toml.example");
565
605
  // El template distribuye wrangler.toml.example; init genera el wrangler.toml real.
@@ -569,15 +609,21 @@ function stampWrangler(dir, answers) {
569
609
  if (!existsSync(wt)) return null;
570
610
  let s = readFileSync(wt, "utf8");
571
611
  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";
612
+ // Identidad ÚNICA por instalación: cada bot genera su propio uid de 6 chars y
613
+ // lo usa en worker/D1/Vectorize. Así dos instalaciones en la MISMA cuenta de
614
+ // Cloudflare nunca comparten datos. Si el wrangler.toml ya trae un uid (reinstall
615
+ // en la misma carpeta), se reutiliza.
616
+ const existingUid = (s.match(/name\s*=\s*"kooni-bot-.+-([a-f0-9]{6})"/) || [])[1];
617
+ const uid = existingUid || botUid || randomUUID().replace(/-/g, "").slice(0, 6);
618
+ const resId = slug.replace(/-/g, "_");
619
+ const dbName = `kooni_${resId}_${uid}_db`;
620
+ const kbName = `kooni_${resId}_${uid}_kb`;
621
+ const workerName = `kooni-bot-${slug}-${uid}`;
576
622
  const R = REGIONS[answers.lang] || REGIONS["es-MX"];
577
623
 
578
624
  const set = (re, val) => { s = s.replace(re, val); };
579
625
 
580
- set(/^name\s*=\s*"[^"]*"/m, `name = "kooni-bot-${slug}"`);
626
+ set(/^name\s*=\s*"[^"]*"/m, `name = "${workerName}"`);
581
627
  set(/BOT_NAME\s*=\s*"[^"]*"/g, `BOT_NAME = "${String(answers.botName).replace(/"/g, "'")}"`);
582
628
  set(/BUSINESS_NAME\s*=\s*"[^"]*"/g, `BUSINESS_NAME = "${String(answers.businessName).replace(/"/g, "'")}"`);
583
629
  set(/BOT_LANGUAGE\s*=\s*"[^"]*"/g, `BOT_LANGUAGE = "${answers.lang}"`);
@@ -597,6 +643,13 @@ function stampWrangler(dir, answers) {
597
643
  return new RegExp(`^\\s*${key}\\s*=`, "m").test(varsMatch[0]);
598
644
  };
599
645
 
646
+ // Identidad de instalación para ligar licencias (ver src/license.ts + limits.ts).
647
+ if (hasInVars("BOT_INSTANCE_ID")) {
648
+ set(/BOT_INSTANCE_ID\s*=\s*"[^"]*"/g, `BOT_INSTANCE_ID = "${uid}"`);
649
+ } else {
650
+ s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1BOT_INSTANCE_ID = "${uid}"\n`);
651
+ }
652
+
600
653
  if (answers.provider !== "anthropic") {
601
654
  if (hasInVars("LLM_PROVIDER")) {
602
655
  set(/LLM_PROVIDER\s*=\s*"[^"]*"/g, `LLM_PROVIDER = "${answers.provider}"`);
@@ -617,7 +670,7 @@ function stampWrangler(dir, answers) {
617
670
  }
618
671
 
619
672
  writeFileSync(wt, s);
620
- return { dbName, kbName, tz: R.tz, currency: R.currency };
673
+ return { uid, dbName, kbName, workerName, tz: R.tz, currency: R.currency };
621
674
  }
622
675
 
623
676
  function renderMemberConfig(answers, meta) {
@@ -689,13 +742,14 @@ function writeDevVars(dir, answers, kbToken) {
689
742
 
690
743
  function collectAnswers(flags) {
691
744
  const rawBrain = String(flags.cerebro || flags.brain || "").trim().toLowerCase();
692
- const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", gateway: "gateway" })[rawBrain] || null;
745
+ const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", minimax: "minimax", gateway: "gateway" })[rawBrain] || null;
693
746
  const tone = ({ cercano: "cercano", friendly: "cercano", formal: "formal", divertido: "divertido", playful: "divertido" })[String(flags.tono || "").trim().toLowerCase()] || null;
694
747
 
695
748
  // Lo que vino por flag se conserva; lo que NO vino queda undefined para que
696
749
  // `onboarding()` lo pregunte (interactivo) o use el default (no-interactivo).
697
750
  return {
698
751
  slug: flags.slug ? sanitizeSlug(flags.slug) : undefined,
752
+ uid: String(flags.uid || "").trim().toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 6) || undefined,
699
753
  businessName: String(flags.negocio || flags.nombre || flags.name || "").trim() || undefined,
700
754
  botName: String(flags["bot-name"] || "").trim() || undefined,
701
755
  lang: flags.lang ? normBotLang(flags.lang) : undefined,
@@ -740,7 +794,7 @@ async function onboarding(rl, answers, defaultDir) {
740
794
  // hace después desde el dashboard. Aquí solo fijamos el default.
741
795
  answers.tier = answers.tier || "free";
742
796
 
743
- const brainKeys = ["claude", "chatgpt", "grok", "gateway"];
797
+ const brainKeys = ["claude", "chatgpt", "grok", "minimax", "gateway"];
744
798
  const brainIdx = await select(rl, t().brainQ, brainKeys.map((k) => ({
745
799
  key: k, label: BRAINS[k].label,
746
800
  desc: k === "claude" ? t().brainDesc : "",
@@ -821,10 +875,11 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
821
875
  wrangler(dir, ["login"]);
822
876
  console.log(" " + C.green("✓") + " " + t().loginOk);
823
877
 
824
- // recursos (nombres canónicos del proyecto: kooni_db / kooni_kb)
878
+ // recursos (nombres ÚNICOS por instalación, ya estampados en wrangler.toml)
825
879
  console.log("\n " + C.dim(t().creatingResources));
826
- const dbName = "kooni_db";
827
- const kbName = "kooni_kb";
880
+ const wtRaw = readFileSync(wt, "utf8");
881
+ const dbName = (wtRaw.match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
882
+ const kbName = (wtRaw.match(/index_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_kb";
828
883
 
829
884
  let d1Id = "";
830
885
  try {
@@ -904,14 +959,15 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
904
959
  console.log(" " + C.green("✓") + " " + m("dependencias listas", "dependencies ready"));
905
960
 
906
961
  console.log(" " + C.dim(t().migrations));
907
- runPnpm(dir, ["db:apply:remote"]);
962
+ wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]);
908
963
  console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
909
964
 
910
965
  console.log(" " + C.dim(t().deploying));
911
966
  let url = "";
912
967
  try {
913
968
  const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
914
- url = (dep.match(/https:\/\/[a-z0-9-]+\.workers\.dev/) || [])[0] || "";
969
+ // La URL real incluye el subdominio de la cuenta: <worker>.<cuenta>.workers.dev
970
+ url = (dep.match(/https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev/) || [])[0] || "";
915
971
  } catch (e) {
916
972
  // Muestra el detalle real del deploy en vez de tragarlo: así el usuario ve
917
973
  // qué falló (deploy-check, binding, auth…) y puede corregirlo.
@@ -925,6 +981,8 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
925
981
  } else {
926
982
  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
983
  }
984
+ // Persistir identidad de Cloudflare en el marker (para update/doctor/selector).
985
+ writeMarker(dir, { databaseId: d1Id, workerUrl: url || undefined, dbName, kbName });
928
986
  return url;
929
987
  }
930
988
 
@@ -932,14 +990,19 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
932
990
  async function checkin(dir, answers, version) {
933
991
  if (process.env.KOONI_NO_CHECKIN === "1" || process.env.KOONI_SILENT === "1") return;
934
992
  try {
935
- const slug = answers.slug || basename(dir);
993
+ const marker = readMarker(dir) || {};
994
+ const slug = answers.slug || marker.slug || basename(dir);
936
995
  await fetchTimeout(CHECKIN_URL, {
937
996
  method: "POST",
938
997
  headers: { "Content-Type": "application/json" },
939
998
  body: JSON.stringify({
940
999
  email: answers.email || undefined,
941
1000
  slug,
942
- workerUrl: `https://kooni-bot-${slug}.workers.dev`,
1001
+ uid: marker.uid,
1002
+ workerName: marker.workerName,
1003
+ dbName: marker.dbName,
1004
+ kbName: marker.kbName,
1005
+ workerUrl: marker.workerUrl || `https://kooni-bot-${slug}.workers.dev`,
943
1006
  cliVersion: CLI_VERSION,
944
1007
  botVersion: version,
945
1008
  tier: answers.tier,
@@ -973,9 +1036,15 @@ constructor. No hay licencia ni servidor de Horizontes: el tier free/pro se cont
973
1036
  - \`npx kooni-bot init [dir]\` — descarga el template, configura (idioma, negocio, cerebro) y ofrece desplegar.
974
1037
  - \`npx kooni-bot deploy [dir]\` — provisiona Cloudflare (login, D1/Vectorize/R2, secrets, migraciones, deploy).
975
1038
  - \`npx kooni-bot update [dir]\` — trae la versión nueva conservando \`member/\`, \`wrangler.toml\` y datos.
1039
+ - \`npx kooni-bot update --all\` — actualiza TODAS las instalaciones registradas en esta computadora.
976
1040
  - \`npx kooni-bot doctor [dir]\` — diagnostica el bot instalado.
977
1041
  - \`npx kooni-bot version\`.
978
1042
 
1043
+ ## Conexión de canales (DESDE el panel, sin redeploy)
1044
+ - 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.
1045
+ - **Telegram registra su webhook automáticamente** al guardar el token (setWebhook apuntando a <worker>/webhooks/telegram). En la misma card se puede poner el chat id del dueño para los avisos de handoff (alternativa al secret \`OWNER_TELEGRAM_CHAT_ID\`).
1046
+ - La URL del webhook de cada canal se muestra en su propia card (con botón copiar).
1047
+
979
1048
  ## Regla de oro (memorízala)
980
1049
  | Carpeta / archivo | Qué pasa al actualizar |
981
1050
  |---|---|
@@ -996,7 +1065,7 @@ Si dudas: **member/ es sagrado, src/ se actualiza.**
996
1065
  ## Secrets y vars (referencia rápida)
997
1066
  - Secrets: \`ANTHROPIC_API_KEY\` / \`OPENAI_API_KEY\` / \`XAI_API_KEY\` (cerebro), \`DASHBOARD_PASSWORD\` (panel), \`KB_REINDEX_TOKEN\` (reindex).
998
1067
  - Canales: \`TELEGRAM_BOT_TOKEN\`, \`MANYCHAT_API_KEY\`, \`TWILIO_ACCOUNT_SID\`+\`TWILIO_AUTH_TOKEN\`+\`TWILIO_WA_FROM\`, \`META_PAGE_ACCESS_TOKEN\`+\`META_VERIFY_TOKEN\`+\`META_APP_SECRET\`, \`ZERNIO_API_KEY\`.
999
- - Avisos al dueño: \`OWNER_TELEGRAM_CHAT_ID\`, \`RESEND_API_KEY\`+\`OWNER_EMAIL\`, \`OWNER_WA_NUMBER\`.
1068
+ - Avisos al dueño: \`OWNER_TELEGRAM_CHAT_ID\` (o el campo "chat id" de la card Telegram en Conexiones), \`RESEND_API_KEY\`+\`OWNER_EMAIL\`, \`OWNER_WA_NUMBER\`.
1000
1069
  - Vars en \`wrangler.toml\`: \`BOT_NAME\`, \`BUSINESS_NAME\`, \`BOT_LANGUAGE\`, \`BOT_TIER\`, \`BUFFER_SECONDS\`, \`DASHBOARD_BASE_URL\`, \`LLM_PROVIDER\` (\`anthropic\` default | \`openai\` | \`xai\`).
1001
1070
 
1002
1071
  ## Comandos del proyecto (dentro de la carpeta del bot, con pnpm)
@@ -1079,7 +1148,7 @@ async function cmdInit(flags, rest) {
1079
1148
  const answers = collectAnswers(flags);
1080
1149
  await onboarding(rl, answers, basename(dir));
1081
1150
 
1082
- const meta = stampWrangler(dir, answers);
1151
+ const meta = stampWrangler(dir, answers, answers.uid);
1083
1152
  const kbToken = "kooni-reindex-" + randomUUID().replace(/-/g, "").slice(0, 12);
1084
1153
  if (existsSync(join(dir, "member"))) {
1085
1154
  const region = REGIONS[answers.lang] || REGIONS["es-MX"];
@@ -1090,8 +1159,26 @@ async function cmdInit(flags, rest) {
1090
1159
  }));
1091
1160
  }
1092
1161
  writeDevVars(dir, answers, kbToken);
1093
- writeMarker(dir, { slug: answers.slug, version, lang: L });
1162
+ writeMarker(dir, {
1163
+ slug: answers.slug,
1164
+ version,
1165
+ lang: L,
1166
+ uid: (meta && meta.uid) || answers.uid,
1167
+ workerName: meta && meta.workerName,
1168
+ dbName: meta && meta.dbName,
1169
+ kbName: meta && meta.kbName,
1170
+ });
1094
1171
  console.log("\n " + C.green("✓") + " " + t().configDone);
1172
+ console.log(" " + C.green("✓") + " " + m(`Bot instalado → ${dir}`, `Bot installed → ${dir}`));
1173
+
1174
+ // Registrar la instalación local para el selector multi-bot.
1175
+ recordInstall(dir, {
1176
+ slug: answers.slug,
1177
+ uid: (meta && meta.uid) || answers.uid,
1178
+ workerName: meta && meta.workerName,
1179
+ dbName: meta && meta.dbName,
1180
+ kbName: meta && meta.kbName,
1181
+ });
1095
1182
 
1096
1183
  // deploy (si no lo deshabilitan)
1097
1184
  if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
@@ -1100,9 +1187,16 @@ async function cmdInit(flags, rest) {
1100
1187
  if (url) {
1101
1188
  console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
1102
1189
  console.log(" " + C.cyan(t().panel) + " " + C.b(url + "/admin"));
1103
- console.log(" " + C.dim(t().next) + "\n");
1190
+ console.log("\n " + C.b(m("Lo que sigue (tu agente de Claude Code lo hace por ti):", "Next steps (your Claude Code agent does them for you):")));
1191
+ console.log(" 1. " + C.dim(m("abre tu panel en el link de arriba (usuario admin + tu contraseña)", "open your panel at the link above (user admin + your password)")));
1192
+ console.log(" 2. " + C.dim(m("Conexiones → pega tu token de Telegram: lo valida y registra el webhook solo", "Connections → paste your Telegram token: it validates and registers the webhook automatically")));
1193
+ console.log(" 3. " + C.dim(m("en la misma card, pega tu chat id (mándale /start a tu bot y míralo con @userinfobot) para recibir los avisos de handoff", "on the same card, paste your chat id (send /start to your bot and see it with @userinfobot) to get handoff alerts")));
1194
+ console.log(" 4. " + C.dim(m("conecta el resto de canales (Zernio, WhatsApp…) desde el mismo panel — se ponen verdes al instante", "connect the other channels (Zernio, WhatsApp…) from the same panel — they turn green instantly")));
1195
+ console.log("\n " + C.dim(m("Actualiza cuando saquemos mejoras: npx kooni-bot update", "Update when we ship improvements: npx kooni-bot update")) + "\n");
1104
1196
  } else {
1105
- console.log(" " + C.yellow(m("No detecté la URL automáticamente. Búscala en la salida de `pnpm run deploy` arriba (https://…workers.dev) y abre <url>/admin.", "Couldn't auto-detect the URL. Find it in the `pnpm run deploy` output above (https://…workers.dev) and open <url>/admin.")) + "\n");
1197
+ console.log("\n " + C.yellow(m("No desplegaste todavía. Cuando quieras:", "You haven't deployed yet. When you're ready:")));
1198
+ console.log(" " + C.cyan("npx kooni-bot deploy"));
1199
+ console.log(" " + C.dim(m("levanta el bot en TU Cloudflare y te da la URL del panel /admin.", "it deploys the bot on YOUR Cloudflare and gives you the /admin panel URL.")) + "\n");
1106
1200
  }
1107
1201
  }
1108
1202
 
@@ -1122,17 +1216,17 @@ async function cmdDeploy(flags, rest) {
1122
1216
  banner();
1123
1217
  installAgentSkill(flags);
1124
1218
 
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
1219
  const rl = createInterface({ input, output });
1129
1220
  try {
1221
+ const dir = await resolveBotDir(rest[0], rl);
1222
+ if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1223
+
1130
1224
  // provider para elegir el secret correcto
1131
1225
  let brainKey = "claude";
1132
1226
  try {
1133
1227
  const wt = readFileSync(join(dir, "wrangler.toml"), "utf8");
1134
1228
  const p = (wt.match(/LLM_PROVIDER\s*=\s*"([^"]+)"/) || [])[1] || "anthropic";
1135
- brainKey = ({ anthropic: "claude", openai: "chatgpt", xai: "grok" })[p] || "claude";
1229
+ brainKey = ({ anthropic: "claude", openai: "chatgpt", xai: "grok", minimax: "minimax" })[p] || "claude";
1136
1230
  } catch {}
1137
1231
  flags.brainKey = brainKey;
1138
1232
  await deployBot(dir, { flags, rl });
@@ -1144,24 +1238,16 @@ async function cmdDeploy(flags, rest) {
1144
1238
  }
1145
1239
  }
1146
1240
 
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
-
1241
+ // Actualiza UNA instalación. Devuelve un resumen; NO hace process.exit (para que
1242
+ // `--all` pueda recorrer varias sin cortar el proceso).
1243
+ async function updateOne(dir, flags = {}) {
1157
1244
  const marker = readMarker(dir) || {};
1245
+ const slug = marker.slug || basename(dir);
1158
1246
  const current = marker.version || readPkgVersion(dir) || "0.0.0";
1159
1247
 
1160
1248
  const tgz = join(dir, ".kooni-template.tgz");
1161
- process.stdout.write(C.dim(" " + t().updRevalidating + "\n"));
1162
1249
  await downloadTemplate(tgz);
1163
1250
 
1164
- // versión nueva desde el tarball
1165
1251
  const tmp = join(dir, ".kooni-extract");
1166
1252
  mkdirSync(tmp, { recursive: true });
1167
1253
  const src = extractToTemp(tgz, tmp);
@@ -1176,29 +1262,26 @@ async function cmdUpdate(flags, rest) {
1176
1262
  if (!verLt(current, next)) {
1177
1263
  rmSync(tmp, { recursive: true, force: true });
1178
1264
  rmSync(tgz, { force: true });
1179
- console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + current + ")\n");
1180
- return;
1265
+ return { dir, slug, updated: false, from: current, to: next };
1181
1266
  }
1182
1267
 
1183
- // respaldo + extraer sobre la instalación
1184
1268
  const backupPath = backupBeforeUpdate(dir, current);
1185
1269
  extractOver(tgz, dir);
1186
- writeMarker(dir, { slug: marker.slug || basename(dir), version: next, lang: marker.lang || L });
1270
+ writeMarker(dir, { slug, version: next, lang: marker.lang || L });
1187
1271
 
1188
1272
  console.log(" " + C.green("✓") + " " + t().updDone(next));
1189
1273
  if (backupPath) console.log(" " + C.dim(t().updBackup(backupPath.slice(dir.length + 1))));
1190
1274
  console.log(" " + C.dim(t().updPreserved));
1191
1275
  console.log(" " + C.dim(t().updReplaced));
1192
1276
 
1193
- // dependencias + migraciones + reindex + deploy
1194
1277
  console.log("\n " + C.dim(t().installing));
1195
1278
  runPnpm(dir, ["install"]);
1196
- try { runPnpm(dir, ["db:apply:remote"]); } catch { /* best-effort */ }
1279
+ const dbName = marker.dbName || (readFileSync(join(dir, "wrangler.toml"), "utf8").match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
1280
+ try { wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]); } catch { /* best-effort */ }
1197
1281
  try { runPnpm(dir, ["kb:reindex"]); } catch { /* best-effort */ }
1198
1282
  console.log(" " + C.dim(t().deploying));
1199
1283
  try { runPnpm(dir, ["run", "deploy"]); } catch { /* el deploy-check imprime el detalle */ }
1200
1284
 
1201
- // reindex del worker si hay estado con URL
1202
1285
  try {
1203
1286
  const st = JSON.parse(readFileSync(join(dir, ".bot-state.json"), "utf8"));
1204
1287
  if (st.worker_url) {
@@ -1208,6 +1291,60 @@ async function cmdUpdate(flags, rest) {
1208
1291
  }
1209
1292
  } catch {}
1210
1293
 
1294
+ return { dir, slug, updated: true, from: current, to: next };
1295
+ }
1296
+
1297
+ async function cmdUpdate(flags, rest) {
1298
+ const cfg = loadCfg();
1299
+ ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
1300
+ if (flags.lang === "en" || cfg.lang === "en") L = "en";
1301
+ banner();
1302
+ installAgentSkill(flags);
1303
+
1304
+ // --all: actualiza todas las instalaciones registradas en ~/.kooni/installs.json.
1305
+ if (flags.all) {
1306
+ const dirs = listInstalls().map((x) => x.dir);
1307
+ if (dirs.length === 0) {
1308
+ console.log(" " + C.red(t().needDir) + " (sin instalaciones registradas)\n");
1309
+ process.exit(1);
1310
+ }
1311
+ console.log(" " + C.dim(m(`Actualizando ${dirs.length} instalaciones…`, `Updating ${dirs.length} installs…`)) + "\n");
1312
+ const results = [];
1313
+ for (const d of dirs) {
1314
+ const marker = readMarker(d) || {};
1315
+ const slug = marker.slug || basename(d);
1316
+ console.log(C.b("\n ◇ " + slug));
1317
+ try {
1318
+ const r = await updateOne(d, flags);
1319
+ results.push(r);
1320
+ console.log(r.updated
1321
+ ? " " + C.green(`✓ ${slug}: ${r.from} → ${r.to}`)
1322
+ : " " + C.green(`✓ ${slug}: ${t().updUpToDate} (v${r.to})`));
1323
+ } catch (e) {
1324
+ results.push({ dir: d, slug, error: e.message || String(e) });
1325
+ console.log(" " + C.red(`✗ ${slug}: ${e.message || e}`));
1326
+ }
1327
+ }
1328
+ console.log("");
1329
+ return;
1330
+ }
1331
+
1332
+ const rl = createInterface({ input, output });
1333
+ let dir;
1334
+ try {
1335
+ dir = await resolveBotDir(rest[0], rl);
1336
+ } finally {
1337
+ rl.close();
1338
+ }
1339
+ if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1340
+
1341
+ const marker = readMarker(dir) || {};
1342
+ const slug = marker.slug || basename(dir);
1343
+ console.log(C.b("\n ◇ " + slug));
1344
+ const r = await updateOne(dir, flags);
1345
+ if (!r.updated) {
1346
+ console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + r.to + ")\n");
1347
+ }
1211
1348
  console.log("");
1212
1349
  }
1213
1350
 
@@ -1220,7 +1357,13 @@ async function cmdDoctor(flags, rest) {
1220
1357
  const warn = (s, h) => { console.log(" " + C.yellow("⚠") + " " + s); if (h) console.log(" " + C.dim(h)); };
1221
1358
  const bad = (s, h) => { console.log(" " + C.red("✗") + " " + s); if (h) console.log(" " + C.dim(h)); };
1222
1359
 
1223
- const dir = resolveBotDir(rest[0]);
1360
+ const rl = createInterface({ input, output });
1361
+ let dir;
1362
+ try {
1363
+ dir = await resolveBotDir(rest[0], rl);
1364
+ } finally {
1365
+ rl.close();
1366
+ }
1224
1367
  if (!dir) { bad(t().needDir + " " + (rest[0] || process.cwd())); process.exit(1); }
1225
1368
  ok(m("Bot encontrado en ", "Bot found in ") + C.cyan(dir));
1226
1369
 
@@ -1279,6 +1422,7 @@ ${C.cyan("kooni-bot")} — ${t().helpIntro}
1279
1422
  ${C.cyan("npx kooni-bot init [dir]")} ${m("instala (descarga template + config + deploy)", "install (download template + config + deploy)")}
1280
1423
  ${C.cyan("npx kooni-bot deploy [dir]")} ${m("provisiona Cloudflare y publica el worker", "provision Cloudflare and publish the worker")}
1281
1424
  ${C.cyan("npx kooni-bot update [dir]")} ${m("actualiza sin perder tu configuración", "update without losing config")}
1425
+ ${C.cyan("npx kooni-bot update --all")} ${m("actualiza TODAS las instalaciones registradas", "update ALL registered installs")}
1282
1426
  ${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
1283
1427
  ${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
1284
1428
 
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
- {
2
- "name": "kooni-bot",
3
- "version": "0.2.5",
4
- "description": "Kooni — instala tu asistente de IA multicanal (WhatsApp, Instagram, Messenger, Telegram) en TU Cloudflare, en un comando.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "kooni-bot": "bin/kooni.js"
9
- },
10
- "engines": {
11
- "node": ">=18"
12
- },
13
- "keywords": [
14
- "chatbot",
15
- "ai",
16
- "whatsapp",
17
- "instagram",
18
- "telegram",
19
- "cloudflare",
20
- "kooni"
21
- ],
22
- "files": [
23
- "bin"
24
- ]
1
+ {
2
+ "name": "kooni-bot",
3
+ "version": "0.2.9",
4
+ "description": "Kooni — instala tu asistente de IA multicanal (WhatsApp, Instagram, Messenger, Telegram) en TU Cloudflare, en un comando.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "kooni-bot": "bin/kooni.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "keywords": [
14
+ "chatbot",
15
+ "ai",
16
+ "whatsapp",
17
+ "instagram",
18
+ "telegram",
19
+ "cloudflare",
20
+ "kooni"
21
+ ],
22
+ "files": [
23
+ "bin"
24
+ ]
25
25
  }