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.
- package/bin/kooni.js +195 -51
- 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.
|
|
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,
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
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 = "
|
|
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
|
|
878
|
+
// recursos (nombres ÚNICOS por instalación, ya estampados en wrangler.toml)
|
|
825
879
|
console.log("\n " + C.dim(t().creatingResources));
|
|
826
|
-
const
|
|
827
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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, {
|
|
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.
|
|
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
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
}
|