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.
- package/bin/kooni.js +55 -44
- 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.
|
|
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.
|
|
343
|
-
// (
|
|
344
|
-
//
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
498
|
-
// -
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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,
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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:
|
|
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