kooni-bot 0.2.1 → 0.2.3

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 +55 -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.1";
25
+ const CLI_VERSION = "0.2.3";
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)",
@@ -339,27 +339,30 @@ async function ask(rl, q, val) {
339
339
  return (await rl.question("\n " + C.b(q) + "\n " + C.cyan("› "))).trim();
340
340
  }
341
341
 
342
- // Input oculto para API keys / contraseñas. Usa una interfaz readline DEDICADA
343
- // (no el rl compartido) con máscara de salida para no chocar con los listeners
344
- // del selector y soportar pegado. Devuelve "" si no hay TTY.
345
- async function promptSecret(q) {
342
+ // Input oculto para API keys / contraseñas. Reutiliza la MISMA interfaz `rl`
343
+ // compartida (crear una segunda readline sobre el mismo stdin competía por los
344
+ // datos y nunca recibía Enter el proceso se quedaba colgado). Enmascara el eco
345
+ // parcheando temporalmente `_writeToOutput`: deja pasar solo los saltos de línea.
346
+ async function promptSecret(rl, q) {
346
347
  if (!interactive()) return "";
347
- const rl = createInterface({ input, output, terminal: true });
348
- const answer = await new Promise((resolve) => {
349
- // Enmascara lo que teclea el usuario: solo deja pasar el prompt y los saltos
350
- // de línea (nunca la tecla), para que la API key quede oculta.
351
- const orig = rl._writeToOutput;
352
- rl._writeToOutput = (s) => {
353
- if (s === "\r\n" || s === "\n" || s === "\r") process.stdout.write(s);
354
- else if (typeof orig === "function" && s.includes(" ")) orig.call(rl, s);
355
- };
356
- rl.question("\n " + C.b(q) + "\n " + C.cyan("› "), (a) => {
357
- rl.close();
358
- resolve(a);
359
- });
360
- });
361
- process.stdout.write("\n");
362
- return answer.trim();
348
+ process.stdout.write("\n " + C.b(q) + "\n " + C.cyan("› "));
349
+
350
+ const orig = rl._writeToOutput;
351
+ let masking = true;
352
+ rl._writeToOutput = (s) => {
353
+ if (!masking) return typeof orig === "function" ? orig.call(rl, s) : process.stdout.write(s);
354
+ // Enmascara lo tecleado; solo deja los saltos de línea.
355
+ if (s === "\r\n" || s === "\n" || s === "\r") process.stdout.write(s);
356
+ };
357
+
358
+ try {
359
+ const answer = await rl.question("");
360
+ return answer.trim();
361
+ } finally {
362
+ rl._writeToOutput = orig;
363
+ masking = false;
364
+ process.stdout.write("\n");
365
+ }
363
366
  }
364
367
 
365
368
  async function confirm(rl, q) {
@@ -494,24 +497,27 @@ function resolveBotDir(arg) {
494
497
  // ── exec cross-platform ──────────────────────────────────────────────────────
495
498
  const isWin = process.platform === "win32";
496
499
 
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.
500
+ // Ejecuta un binario de forma segura en Windows/macOS/Linux:
501
+ // - Unix/mac: `spawnSync(file, args)` directo.
502
+ // - Windows: `npx`/`pnpm` son shims `.cmd`, que NO se pueden spawnear directo
503
+ // (EINVAL). Se lanzan con `cmd.exe /c <file> <args...>` pasando los argumentos
504
+ // como lista separada (nada de `shell: true` ni concatenación sin DEP0190).
505
+ // - `stdio` hereda por defecto (progreso en vivo); `capture` captura para parsear.
502
506
  function run(file, args = [], opts = {}) {
503
507
  const cwd = opts.cwd;
504
508
  const input = opts.input != null ? String(opts.input) : undefined;
505
509
  const stdio = opts.stdio || (opts.capture ? ["pipe", "pipe", "pipe"] : "inherit");
506
510
 
507
511
  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";
512
+ let argv = args;
513
+
514
+ if (isWin) {
515
+ const shim = { npx: "npx.cmd", pnpm: "pnpm.cmd" }[file] || file;
516
+ cmd = "cmd.exe";
517
+ argv = ["/d", "/c", shim, ...args];
512
518
  }
513
519
 
514
- const r = spawnSync(cmd, args, { cwd, input, stdio, encoding: "utf8" });
520
+ const r = spawnSync(cmd, argv, { cwd, input, stdio, encoding: "utf8" });
515
521
  if (r.error) throw r.error;
516
522
  if (r.status !== 0) {
517
523
  const err = new Error(`Command failed: ${[cmd, ...args].join(" ")}`);
@@ -692,6 +698,7 @@ function collectAnswers(flags) {
692
698
  brainKey,
693
699
  baseUrl: String(flags["base-url"] || "").trim() || undefined,
694
700
  secret: brainKey ? BRAINS[brainKey].secret : undefined,
701
+ apiKey: String(flags["api-key"] || "").trim() || undefined,
695
702
  what: String(flags.que || "").trim() || undefined,
696
703
  offer: String(flags.ofrece || "").trim() || undefined,
697
704
  hours: String(flags.horario || "").trim() || undefined,
@@ -723,11 +730,9 @@ async function onboarding(rl, answers, defaultDir) {
723
730
  const langIdx = await select(rl, t().qLang, langKeys.map((k) => ({ key: k, label: k })), { value: answers.lang, default: 0 });
724
731
  answers.lang = langKeys[langIdx] || "es-MX";
725
732
 
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";
733
+ // El plan NO se pregunta: se instala en free y la activación de plan/clave se
734
+ // hace después desde el dashboard. Aquí solo fijamos el default.
735
+ answers.tier = answers.tier || "free";
731
736
 
732
737
  const brainKeys = ["claude", "chatgpt", "grok", "gateway"];
733
738
  const brainIdx = await select(rl, t().brainQ, brainKeys.map((k) => ({
@@ -741,6 +746,12 @@ async function onboarding(rl, answers, defaultDir) {
741
746
  answers.baseUrl = await ask(rl, t().qBaseUrl, undefined) || "https://api.aisa.one/v1";
742
747
  }
743
748
 
749
+ // La API key se pide aquí, apenas se elige el cerebro — y se conserva para el
750
+ // deploy (se guarda como secret sin mostrarla). En no-interactivo se salta.
751
+ if (!answers.apiKey) {
752
+ answers.apiKey = await promptSecret(rl, t().qApiKey(BRAINS[answers.brainKey].label));
753
+ }
754
+
744
755
  answers.what = answers.what ?? (await ask(rl, t().qWhat, undefined) || "");
745
756
  answers.offer = answers.offer ?? (await ask(rl, t().qOffer, undefined) || "");
746
757
  answers.hours = answers.hours ?? (await ask(rl, t().qHours, undefined) || "");
@@ -850,11 +861,9 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
850
861
  // secrets (sin mostrarlos)
851
862
  console.log("\n " + C.dim(t().secretsTitle));
852
863
 
853
- // API key: interactiva por defecto; en no-interactivo se delega al agente.
864
+ // API key: viene del onboarding (o de --api-key). Si no está y no-interactivo,
865
+ // se delega al agente (wrangler secret put). No se vuelve a preguntar aquí.
854
866
  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
867
  if (apiKey) {
859
868
  wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], { input: apiKey, capture: true });
860
869
  console.log(" " + C.green("✓") + " " + t().secretOk(BRAINS[flags.brainKey || "claude"].secret));
@@ -862,10 +871,12 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
862
871
  console.log(C.yellow("\n ── PARA EL AGENTE ── falta la API key de IA."));
863
872
  console.log(" " + C.cyan(`npx wrangler secret put ${BRAINS[flags.brainKey || "claude"].secret}`));
864
873
  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)."));
874
+ } else {
875
+ 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
876
  }
866
877
 
867
878
  let dash = "";
868
- if (interactive()) dash = await promptSecret(m("Elige una contraseña para el panel /admin (usuario: admin):", "Choose a password for the /admin dashboard (user: admin):"));
879
+ if (interactive()) dash = await promptSecret(rl, m("Elige una contraseña para el panel /admin (usuario: admin):", "Choose a password for the /admin dashboard (user: admin):"));
869
880
  if (!dash) dash = "kooni-local-password";
870
881
  wrangler(dir, ["secret", "put", "DASHBOARD_PASSWORD"], { input: dash, capture: true });
871
882
  console.log(" " + C.green("✓") + " " + t().secretOk("DASHBOARD_PASSWORD"));
@@ -1075,7 +1086,7 @@ async function cmdInit(flags, rest) {
1075
1086
 
1076
1087
  // deploy (si no lo deshabilitan)
1077
1088
  if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
1078
- const deployFlags = { ...flags, brainKey: answers.brainKey };
1089
+ const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
1079
1090
  const url = await deployBot(dir, { flags: deployFlags, rl });
1080
1091
  if (url) {
1081
1092
  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.3",
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",