kooni-bot 0.2.1 → 0.2.2

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 +31 -23
  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.1";
25
+ const CLI_VERSION = "0.2.2";
26
26
 
27
27
  const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
28
28
  const BRANCH = process.env.KOONI_BRANCH || "main";
@@ -67,9 +67,9 @@ const DICT = {
67
67
  qBusiness: "¿Cómo se llama tu negocio?",
68
68
  qBotName: "¿Cómo se llama tu asistente?",
69
69
  qLang: "¿En qué idioma debe hablar tu bot?",
70
- qTier: "¿Qué plan quieres? (free | pro)",
71
70
  brainQ: "¿Con qué cerebro (modelo de IA) quieres que piense tu bot?",
72
71
  brainDesc: "recomendado",
72
+ qApiKey: (label) => `Pega tu API key de ${label} (no se mostrará):`,
73
73
  qBaseUrl: "URL base del gateway (ej. https://api.aisa.one/v1)",
74
74
  qWhat: "En una frase, ¿a qué se dedica?",
75
75
  qOffer: "¿Qué ofreces? (tus servicios o productos principales, con precios si quieres)",
@@ -140,9 +140,9 @@ const DICT = {
140
140
  qBusiness: "What's your business called?",
141
141
  qBotName: "What's your assistant's name?",
142
142
  qLang: "What language should your bot speak?",
143
- qTier: "Which plan do you want? (free | pro)",
144
143
  brainQ: "Which brain (AI model) should your bot think with?",
145
144
  brainDesc: "recommended",
145
+ qApiKey: (label) => `Paste your ${label} API key (hidden):`,
146
146
  qBaseUrl: "Gateway base URL (e.g. https://api.aisa.one/v1)",
147
147
  qWhat: "In one line, what does it do?",
148
148
  qOffer: "What do you offer? (main services or products, with prices if you like)",
@@ -494,24 +494,27 @@ function resolveBotDir(arg) {
494
494
  // ── exec cross-platform ──────────────────────────────────────────────────────
495
495
  const isWin = process.platform === "win32";
496
496
 
497
- // Ejecuta un binario de forma segura en Windows/macOS/Linux SIN `shell: true`:
498
- // - En Windows se resuelve el shim `.cmd` (npx.cmd / pnpm.cmd) y se lanza con
499
- // `spawnSync` (así Node no concatena argumentos ni emite DEP0190).
500
- // - `stdio` hereda por defecto para que el usuario vea el progreso en vivo.
501
- // - Devuelve stdout como string; si el proceso falla, lanza el error con stderr.
497
+ // Ejecuta un binario de forma segura en Windows/macOS/Linux:
498
+ // - Unix/mac: `spawnSync(file, args)` directo.
499
+ // - Windows: `npx`/`pnpm` son shims `.cmd`, que NO se pueden spawnear directo
500
+ // (EINVAL). Se lanzan con `cmd.exe /c <file> <args...>` pasando los argumentos
501
+ // como lista separada (nada de `shell: true` ni concatenación sin DEP0190).
502
+ // - `stdio` hereda por defecto (progreso en vivo); `capture` captura para parsear.
502
503
  function run(file, args = [], opts = {}) {
503
504
  const cwd = opts.cwd;
504
505
  const input = opts.input != null ? String(opts.input) : undefined;
505
506
  const stdio = opts.stdio || (opts.capture ? ["pipe", "pipe", "pipe"] : "inherit");
506
507
 
507
508
  let cmd = file;
508
- if (isWin && !/\.(cmd|bat|exe)$/i.test(file)) {
509
- // pnpm/npx en Windows son shims .cmd; ejecutarlos directo falla con ENOENT/EINVAL.
510
- if (file === "pnpm") cmd = "pnpm.cmd";
511
- else if (file === "npx") cmd = "npx.cmd";
509
+ let argv = args;
510
+
511
+ if (isWin) {
512
+ const shim = { npx: "npx.cmd", pnpm: "pnpm.cmd" }[file] || file;
513
+ cmd = "cmd.exe";
514
+ argv = ["/d", "/c", shim, ...args];
512
515
  }
513
516
 
514
- const r = spawnSync(cmd, args, { cwd, input, stdio, encoding: "utf8" });
517
+ const r = spawnSync(cmd, argv, { cwd, input, stdio, encoding: "utf8" });
515
518
  if (r.error) throw r.error;
516
519
  if (r.status !== 0) {
517
520
  const err = new Error(`Command failed: ${[cmd, ...args].join(" ")}`);
@@ -692,6 +695,7 @@ function collectAnswers(flags) {
692
695
  brainKey,
693
696
  baseUrl: String(flags["base-url"] || "").trim() || undefined,
694
697
  secret: brainKey ? BRAINS[brainKey].secret : undefined,
698
+ apiKey: String(flags["api-key"] || "").trim() || undefined,
695
699
  what: String(flags.que || "").trim() || undefined,
696
700
  offer: String(flags.ofrece || "").trim() || undefined,
697
701
  hours: String(flags.horario || "").trim() || undefined,
@@ -723,11 +727,9 @@ async function onboarding(rl, answers, defaultDir) {
723
727
  const langIdx = await select(rl, t().qLang, langKeys.map((k) => ({ key: k, label: k })), { value: answers.lang, default: 0 });
724
728
  answers.lang = langKeys[langIdx] || "es-MX";
725
729
 
726
- const tierIdx = await select(rl, t().qTier, [
727
- { key: "free", label: "free", desc: m("Starter", "Starter") },
728
- { key: "pro", label: "pro", desc: m("panel completo + tools avanzadas", "full panel + advanced tools") },
729
- ], { value: answers.tier, default: 0 });
730
- answers.tier = tierIdx === 1 ? "pro" : "free";
730
+ // El plan NO se pregunta: se instala en free y la activación de plan/clave se
731
+ // hace después desde el dashboard. Aquí solo fijamos el default.
732
+ answers.tier = answers.tier || "free";
731
733
 
732
734
  const brainKeys = ["claude", "chatgpt", "grok", "gateway"];
733
735
  const brainIdx = await select(rl, t().brainQ, brainKeys.map((k) => ({
@@ -741,6 +743,12 @@ async function onboarding(rl, answers, defaultDir) {
741
743
  answers.baseUrl = await ask(rl, t().qBaseUrl, undefined) || "https://api.aisa.one/v1";
742
744
  }
743
745
 
746
+ // La API key se pide aquí, apenas se elige el cerebro — y se conserva para el
747
+ // deploy (se guarda como secret sin mostrarla). En no-interactivo se salta.
748
+ if (!answers.apiKey) {
749
+ answers.apiKey = await promptSecret(t().qApiKey(BRAINS[answers.brainKey].label));
750
+ }
751
+
744
752
  answers.what = answers.what ?? (await ask(rl, t().qWhat, undefined) || "");
745
753
  answers.offer = answers.offer ?? (await ask(rl, t().qOffer, undefined) || "");
746
754
  answers.hours = answers.hours ?? (await ask(rl, t().qHours, undefined) || "");
@@ -850,11 +858,9 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
850
858
  // secrets (sin mostrarlos)
851
859
  console.log("\n " + C.dim(t().secretsTitle));
852
860
 
853
- // API key: interactiva por defecto; en no-interactivo se delega al agente.
861
+ // API key: viene del onboarding (o de --api-key). Si no está y no-interactivo,
862
+ // se delega al agente (wrangler secret put). No se vuelve a preguntar aquí.
854
863
  let apiKey = flags["api-key"] || "";
855
- if (!apiKey && interactive()) {
856
- apiKey = await promptSecret(m(`Pega tu API key de ${BRAINS[flags.brainKey || "claude"].label} (no se mostrará):`, `Paste your ${BRAINS[flags.brainKey || "claude"].label} API key (hidden):`));
857
- }
858
864
  if (apiKey) {
859
865
  wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], { input: apiKey, capture: true });
860
866
  console.log(" " + C.green("✓") + " " + t().secretOk(BRAINS[flags.brainKey || "claude"].secret));
@@ -862,6 +868,8 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
862
868
  console.log(C.yellow("\n ── PARA EL AGENTE ── falta la API key de IA."));
863
869
  console.log(" " + C.cyan(`npx wrangler secret put ${BRAINS[flags.brainKey || "claude"].secret}`));
864
870
  console.log(" " + m("Pídele la llave al usuario y guárdala tú (entrada oculta).", "Ask the user for the key and save it (hidden input)."));
871
+ } else {
872
+ console.log(" " + C.yellow("⚠") + " " + m("sin API key — el bot no responderá hasta que la pongas desde el panel (Configuración → Modelo de IA).", "no API key — the bot won't reply until you set it from the panel (Settings → AI Model)."));
865
873
  }
866
874
 
867
875
  let dash = "";
@@ -1075,7 +1083,7 @@ async function cmdInit(flags, rest) {
1075
1083
 
1076
1084
  // deploy (si no lo deshabilitan)
1077
1085
  if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
1078
- const deployFlags = { ...flags, brainKey: answers.brainKey };
1086
+ const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
1079
1087
  const url = await deployBot(dir, { flags: deployFlags, rl });
1080
1088
  if (url) {
1081
1089
  console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kooni-bot",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",