kooni-bot 0.2.13 → 0.2.16

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 +261 -13
  2. package/package.json +1 -1
package/bin/kooni.js CHANGED
@@ -19,10 +19,10 @@ import { realpathSync } from "node:fs";
19
19
  import { join, basename } from "node:path";
20
20
  import { homedir } from "node:os";
21
21
  import { execFileSync, spawnSync } from "node:child_process";
22
- import { randomUUID } from "node:crypto";
22
+ import { randomUUID, createHmac } from "node:crypto";
23
23
  import { fileURLToPath } from "node:url";
24
24
 
25
- const CLI_VERSION = "0.2.13";
25
+ const CLI_VERSION = "0.2.16";
26
26
 
27
27
  const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
28
28
  const BRANCH = process.env.KOONI_BRANCH || "main";
@@ -37,6 +37,13 @@ const INSTALLS_FILE = join(CFG_DIR, "installs.json");
37
37
  const MARKER = ".kooni-bot.json";
38
38
  const SKILL_DIR = join(homedir(), ".claude", "skills", "kooni");
39
39
 
40
+ // Llave maestra de licencias (HMAC de los códigos KOONI-PRO-…). DEBE coincidir
41
+ // con el secret LICENSE_MASTER_KEY del panel de licencias (InsForge kooni-licencias)
42
+ // para que los códigos que genera el panel validen en TODAS las instalaciones.
43
+ // Tradeoff conocido de la validación local HMAC: la llave viaja en cada worker.
44
+ // La mejora recomendada (firma asimétrica, pública en el worker) está en PLAN.md.
45
+ const LICENSE_MASTER_KEY = process.env.KOONI_LICENSE_MASTER_KEY || "KqQGmLK7yMl-MISS2JtMd0bCY5lws_a926ksFCIkZkk";
46
+
40
47
  // ── color ─────────────────────────────────────────────────────────────────────
41
48
  const C = {
42
49
  cyan: (s) => `\x1b[36m${s}\x1b[0m`,
@@ -122,6 +129,9 @@ const DICT = {
122
129
  // error
123
130
  templateMissing: "no encontré el instalador en el template.",
124
131
  deployFailed: "el deploy falló. Revisa los mensajes de arriba.",
132
+ noSubdomain: "tu cuenta de Cloudflare no tiene subdominio workers.dev — lo estoy creando automáticamente…",
133
+ subdomainOk: (s) => `subdominio listo: ${s}.workers.dev`,
134
+ subdomainManual: "no pude crear el subdominio automáticamente (no encontré tu sesión de Cloudflare). Hazlo manual, 1 min:",
125
135
  unknown: "comando desconocido:",
126
136
  helpIntro: "instala tu asistente de IA en Cloudflare",
127
137
  },
@@ -189,6 +199,9 @@ const DICT = {
189
199
  qEmail: "Your email? (optional, for support)",
190
200
  templateMissing: "installer not found in template.",
191
201
  deployFailed: "deploy failed. Check the messages above.",
202
+ noSubdomain: "your Cloudflare account has no workers.dev subdomain yet — creating it automatically…",
203
+ subdomainOk: (s) => `subdomain ready: ${s}.workers.dev`,
204
+ subdomainManual: "couldn't create the subdomain automatically (I couldn't find your Cloudflare session). Do it manually, 1 min:",
192
205
  unknown: "unknown command:",
193
206
  helpIntro: "install your AI assistant on Cloudflare",
194
207
  },
@@ -738,6 +751,9 @@ function writeDevVars(dir, answers, kbToken) {
738
751
  lines.push("# " + answers.secret + "=<tu-api-key> ← para wrangler dev local, pégalo aquí (o usa el panel)");
739
752
  lines.push(`DASHBOARD_PASSWORD=${answers.dashPassword || "kooni-local-password"}`);
740
753
  lines.push(`KB_REINDEX_TOKEN=${kbToken}`);
754
+ // Llave maestra de licencias: igual en todas las instalaciones (debe coincidir
755
+ // con la del panel de licencias para que los códigos KOONI-PRO validen).
756
+ lines.push(`LICENSE_MASTER_KEY=${LICENSE_MASTER_KEY}`);
741
757
  writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
742
758
  }
743
759
 
@@ -771,6 +787,7 @@ function collectAnswers(flags) {
771
787
  reglas: String(flags.reglas || "").trim() || undefined,
772
788
  tone,
773
789
  email: String(flags.email || "").trim() || undefined,
790
+ licenseCode: String(flags.license || "").trim() || undefined,
774
791
  };
775
792
  }
776
793
 
@@ -851,9 +868,60 @@ async function onboarding(rl, answers, defaultDir) {
851
868
  ], { value: answers.tone, default: 0 });
852
869
  answers.tone = ["cercano", "formal", "divertido"][toneIdx] || "cercano";
853
870
 
871
+ // Correo del dueño: se pide SIEMPRE (registra la instalación en el panel de
872
+ // licencias y es el canal de contacto/renovación). En interactivo se exige un
873
+ // valor (con reintento); en modo agente/CI viene por --email.
874
+ if (!answers.email) {
875
+ while (interactive()) {
876
+ const em = (await ask(rl, t().qEmail, undefined)).trim();
877
+ if (em) { answers.email = em; break; }
878
+ console.log(" " + C.yellow("⚠") + " " + m("el correo es obligatorio para registrar tu instalación.", "email is required to register your install."));
879
+ }
880
+ }
881
+
882
+ // Licencia Pro: si el usuario dice que es Pro, pide el código y lo valida
883
+ // localmente (HMAC con la master key). Si es válido se guarda tras el deploy
884
+ // (settings pro_license → límites quitados). Si es inválido, se avisa y sigue gratis.
885
+ const wantsPro = interactive()
886
+ ? await confirm(rl, m("¿Tu bot será Pro (tienes una licencia)?", "Will your bot be Pro (do you have a license)?"))
887
+ : !!(flags.license || answers.licenseCode);
888
+ if (wantsPro) {
889
+ const code = answers.licenseCode || (interactive()
890
+ ? await ask(rl, m("Pega tu código KOONI-PRO-…:", "Paste your KOONI-PRO-… code:"), undefined)
891
+ : String(flags.license || "").trim());
892
+ if (code && verifyKooniLicense(code)) {
893
+ answers.licenseCode = code.trim();
894
+ console.log(" " + C.green("✓") + " " + m("licencia válida — se activará al terminar la instalación.", "valid license — it will be activated when the install finishes."));
895
+ } else if (code) {
896
+ console.log(" " + C.yellow("⚠") + " " + m("ese código no es válido. Puedes corregirlo luego en el panel → Licencia.", "that code isn't valid. You can fix it later in the panel → Licencia."));
897
+ }
898
+ }
899
+
854
900
  return answers;
855
901
  }
856
902
 
903
+ // ── licencia Pro ─────────────────────────────────────────────────────────────
904
+ // Valida un código KOONI-PRO-… localmente (mismo formato que src/license.ts del
905
+ // bot): HMAC-SHA256(payload, LICENSE_MASTER_KEY). No exige que esté ligado a
906
+ // esta instalación (eso lo revalida el panel al leerlo).
907
+ function verifyKooniLicense(code) {
908
+ const trimmed = String(code || "").trim();
909
+ const prefix = "KOONI-PRO-";
910
+ if (!trimmed.startsWith(prefix)) return false;
911
+ const rest = trimmed.slice(prefix.length);
912
+ const dot = rest.lastIndexOf(".");
913
+ if (dot <= 0) return false;
914
+ const enc = rest.slice(0, dot);
915
+ const sig = rest.slice(dot + 1);
916
+ try {
917
+ const calc = createHmac("sha256", LICENSE_MASTER_KEY).update(enc).digest("hex");
918
+ if (calc !== sig) return false;
919
+ const payload = JSON.parse(Buffer.from(enc, "base64url").toString("utf8"));
920
+ if (payload.kind === "monthly" && payload.expiry && Date.now() > payload.expiry) return false;
921
+ return true;
922
+ } catch { return false; }
923
+ }
924
+
857
925
  // ── deploy ───────────────────────────────────────────────────────────────────
858
926
  const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
859
927
 
@@ -885,6 +953,114 @@ function patchWranglerFile(dir, fn) {
885
953
  writeFileSync(wt, fn(s));
886
954
  }
887
955
 
956
+ // ── workers.dev subdomain (Cloudflare error 10063) ───────────────────────────
957
+ // Cuando la cuenta aún no tiene subdominio workers.dev, `wrangler deploy` falla
958
+ // con [code: 10063]. Lo resolvemos SOLO con la API de Cloudflare reutilizando la
959
+ // sesión OAuth que wrangler ya guardó en disco (sin pedirle nada al usuario); si
960
+ // no es posible, mostramos los pasos manuales + la opción de API token.
961
+ const CF_OAUTH_CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7"; // cliente OAuth público de wrangler
962
+
963
+ function wranglerConfigCandidates() {
964
+ const h = homedir();
965
+ const appdata = process.env.APPDATA || join(h, "AppData", "Roaming");
966
+ return [
967
+ join(appdata, "xdg.config", ".wrangler", "config", "default.toml"),
968
+ join(h, ".config", ".wrangler", "config", "default.toml"),
969
+ join(h, ".wrangler", "config", "default.toml"),
970
+ join(h, "AppData", "Local", ".wrangler", "config", "default.toml"),
971
+ ].filter((p) => existsSync(p));
972
+ }
973
+
974
+ // Access token OAuth temporal a partir de la sesión guardada de wrangler.
975
+ // IMPORTANTE: Cloudflare ROTA el refresh token en cada intercambio. Si no lo
976
+ // devolvemos al config, invalidamos la sesión de wrangler (error 9109 al
977
+ // siguiente deploy). Por eso: 1) si el access token almacenado aún no expiró,
978
+ // se usa directo (sin tocar el refresh); 2) si hay que refrescar, se persiste
979
+ // el par nuevo de vuelta al config, igual que hace wrangler.
980
+ async function cfOAuthAccessToken() {
981
+ const cfgPath = wranglerConfigCandidates()[0];
982
+ if (!cfgPath) return null;
983
+ let raw = "";
984
+ try { raw = readFileSync(cfgPath, "utf8"); } catch { return null; }
985
+ const pick = (k) => {
986
+ const line = raw.split(/\r?\n/).find((l) => l.trim().startsWith(k + " "));
987
+ if (!line) return "";
988
+ const m = line.match(/=\s*"([^"]*)"/);
989
+ return m ? m[1] : "";
990
+ };
991
+ const storedAccess = pick("oauth_token");
992
+ const exp = pick("expiration_time");
993
+ // 1) Access token almacenado aún vigente → úsalo directo (cero rotación).
994
+ if (storedAccess && exp && new Date(exp).getTime() > Date.now()) {
995
+ try {
996
+ const probe = await fetchRetry("https://api.cloudflare.com/client/v4/accounts", { headers: { Authorization: "Bearer " + storedAccess } }, { ms: 8000, tries: 1 });
997
+ if (probe.ok) return storedAccess;
998
+ } catch {}
999
+ }
1000
+ // 2) Refrescar: intercambia el refresh token y PERSISTE el par nuevo.
1001
+ const refresh = pick("refresh_token") || pick("oauth_token");
1002
+ if (!refresh) return null;
1003
+ try {
1004
+ const res = await fetchRetry("https://dash.cloudflare.com/oauth2/token", {
1005
+ method: "POST",
1006
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1007
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refresh, client_id: CF_OAUTH_CLIENT_ID }),
1008
+ }, { ms: 10000, tries: 2 });
1009
+ if (!res.ok) return null;
1010
+ const j = await res.json().catch(() => ({}));
1011
+ if (!j.access_token) return null;
1012
+ try {
1013
+ const nextRefresh = j.refresh_token || refresh;
1014
+ const expIso = new Date(Date.now() + (j.expires_in || 3599) * 1000).toISOString();
1015
+ const out = raw
1016
+ .replace(/^oauth_token\s*=\s*"[^"]*"/m, `oauth_token = "${j.access_token}"`)
1017
+ .replace(/^refresh_token\s*=\s*"[^"]*"/m, `refresh_token = "${nextRefresh}"`)
1018
+ .replace(/^expiration_time\s*=\s*"[^"]*"/m, `expiration_time = "${expIso}"`);
1019
+ if (out !== raw) writeFileSync(cfgPath, out);
1020
+ } catch {}
1021
+ return j.access_token;
1022
+ } catch { return null; }
1023
+ }
1024
+
1025
+ async function cfApi(access, path, method = "GET", body) {
1026
+ const res = await fetchRetry("https://api.cloudflare.com/client/v4" + path, {
1027
+ method,
1028
+ headers: { Authorization: "Bearer " + access, "Content-Type": "application/json" },
1029
+ body: body ? JSON.stringify(body) : undefined,
1030
+ }, { ms: 15000, tries: 2 });
1031
+ const json = await res.json().catch(() => ({}));
1032
+ return { status: res.status, json };
1033
+ }
1034
+
1035
+ // Nombre candidato para el subdominio, derivado del slug del bot.
1036
+ function proposeSubdomain(slug) {
1037
+ const s = sanitizeSlug(slug || "kooni").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
1038
+ return (s.slice(0, 24).replace(/-+$/, "") || "kooni");
1039
+ }
1040
+
1041
+ async function ensureWorkersDevSubdomain(dir) {
1042
+ const slug = (readMarker(dir) || {}).slug || basename(dir);
1043
+ const access = await cfOAuthAccessToken();
1044
+ if (!access) return { ok: false, reason: "no-oauth" };
1045
+ try {
1046
+ const acc = await cfApi(access, "/accounts");
1047
+ const accounts = (acc.json && acc.json.result) || [];
1048
+ if (!accounts.length) return { ok: false, reason: "no-account" };
1049
+ const account = accounts[0];
1050
+ const sub = await cfApi(access, `/accounts/${account.id}/workers/subdomain`);
1051
+ const existing = sub.json && sub.json.result && sub.json.result.subdomain;
1052
+ if (existing) return { ok: true, subdomain: existing, accountId: account.id, accountName: account.name, via: "existing" };
1053
+ const candidate = proposeSubdomain(slug);
1054
+ const put = await cfApi(access, `/accounts/${account.id}/workers/subdomain`, "PUT", { subdomain: candidate });
1055
+ const created = put.json && put.json.result && put.json.result.subdomain;
1056
+ if (put.status < 300 && created) return { ok: true, subdomain: created, accountId: account.id, accountName: account.name, via: "created" };
1057
+ const msg = (put.json && put.json.errors && put.json.errors[0] && put.json.errors[0].message) || `HTTP ${put.status}`;
1058
+ return { ok: false, reason: "create-failed", message: msg, candidate, accountId: account.id };
1059
+ } catch (e) {
1060
+ return { ok: false, reason: "api-error", message: e.message || String(e) };
1061
+ }
1062
+ }
1063
+
888
1064
  async function deployBot(dir, { flags = {}, rl } = {}) {
889
1065
  const wt = join(dir, "wrangler.toml");
890
1066
  if (!existsSync(wt)) throw new Error(m("no encuentro wrangler.toml en " + dir, "can't find wrangler.toml in " + dir));
@@ -974,6 +1150,33 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
974
1150
  }
975
1151
  } catch {}
976
1152
 
1153
+ // Licencia Pro capturada en el onboarding: se activa después del deploy
1154
+ // escribiendo el código en settings (key pro_license), igual que el panel.
1155
+ const licenseCode = (flags.license || "") ? String(flags.license).trim() : (flags._licenseCode || "");
1156
+ if (flags._licenseCode) {
1157
+ try {
1158
+ const code = String(flags._licenseCode).trim();
1159
+ const now = Date.now();
1160
+ const sql = `INSERT INTO settings (key, value, updated_at) VALUES ('pro_license', '${code.replace(/'/g, "''")}', ${now}) ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at;`;
1161
+ const tmp = join(dir, ".kooni-license.sql");
1162
+ writeFileSync(tmp, sql);
1163
+ wrangler(dir, ["d1", "execute", dbName, "--file=" + tmp, "--remote"], { capture: true });
1164
+ rmSync(tmp, { force: true });
1165
+ console.log(" " + C.green("✓") + " " + m("licencia Pro activada (código guardado)", "Pro license activated (code saved)"));
1166
+ } catch (e) {
1167
+ console.log(" " + C.yellow("⚠") + " " + m("no pude guardar la licencia automáticamente — pégala en el panel → Licencia.", "couldn't save the license automatically — paste it in the panel → Licencia."));
1168
+ }
1169
+ }
1170
+
1171
+ // Llave maestra de licencias: TODAS las instalaciones la llevan, para que los
1172
+ // códigos KOONI-PRO-… generados desde el panel validen aquí.
1173
+ if (LICENSE_MASTER_KEY && LICENSE_MASTER_KEY !== "KqQGmLK7yMl-MISS2JtMd0bCY5lws_a926ksFCIkZkk") {
1174
+ try {
1175
+ wrangler(dir, ["secret", "put", "LICENSE_MASTER_KEY"], { input: LICENSE_MASTER_KEY, capture: true });
1176
+ console.log(" " + C.green("✓") + " " + t().secretOk("LICENSE_MASTER_KEY"));
1177
+ } catch {}
1178
+ }
1179
+
977
1180
  // dependencias + migraciones + deploy (progreso visible en vivo)
978
1181
  console.log("\n " + C.dim(t().installing));
979
1182
  console.log(" " + C.dim(m("esto puede tardar unos minutos la primera vez (descarga el runtime y compila dependencias).", "this may take a few minutes the first time (downloads the runtime and builds native deps).")));
@@ -985,17 +1188,49 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
985
1188
  console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
986
1189
 
987
1190
  console.log(" " + C.dim(t().deploying));
988
- let url = "";
989
- try {
1191
+ const deployOnce = () => {
990
1192
  const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
991
1193
  // La URL real incluye el subdominio de la cuenta: <worker>.<cuenta>.workers.dev
992
- url = (dep.match(/https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev/) || [])[0] || "";
1194
+ return (dep.match(/https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev/) || [])[0] || "";
1195
+ };
1196
+ const deployErrorText = (e) => ((e && (e.stderr || e.stdout || e.message)) || "").toString();
1197
+
1198
+ let url = "";
1199
+ try {
1200
+ url = deployOnce();
993
1201
  } catch (e) {
994
- // Muestra el detalle real del deploy en vez de tragarlo: así el usuario ve
995
- // qué falló (deploy-check, binding, auth…) y puede corregirlo.
996
- const tail = ((e && (e.stderr || e.stdout || e.message)) || "").toString().trim().split("\n").slice(-12).join("\n");
997
- if (tail) console.log(C.red(" ✗ " + tail));
998
- throw new Error(t().deployFailed);
1202
+ const errText = deployErrorText(e);
1203
+ if (/10063|workers\.dev subdomain/.test(errText)) {
1204
+ // La cuenta no tiene subdominio workers.dev: lo creamos automáticamente
1205
+ // con la sesión OAuth de wrangler y reintentamos el deploy.
1206
+ console.log(" " + C.yellow("⚠") + " " + t().noSubdomain);
1207
+ const fix = await ensureWorkersDevSubdomain(dir);
1208
+ if (fix.ok) {
1209
+ console.log(" " + C.green("✓") + " " + t().subdomainOk(fix.subdomain));
1210
+ try {
1211
+ url = deployOnce();
1212
+ } catch (e2) {
1213
+ const tail2 = deployErrorText(e2).trim().split("\n").slice(-12).join("\n");
1214
+ if (tail2) console.log(C.red(" ✗ " + tail2));
1215
+ throw new Error(t().deployFailed);
1216
+ }
1217
+ } else {
1218
+ // Fallback: instrucciones manuales + opción de API token.
1219
+ console.log(C.red(" ✗ " + t().subdomainManual));
1220
+ if (fix.message) console.log(" " + C.dim(fix.message));
1221
+ console.log(" " + C.cyan("https://dash.cloudflare.com/?to=/:account/workers-and-pages") + " " + C.dim(m("→ Workers & Pages → 'Change' junto a 'Your subdomain'", "→ Workers & Pages → 'Change' next to 'Your subdomain'")));
1222
+ console.log(" " + m("o crea un API token (Workers Scripts → Edit) y corre:", "or create an API token (Workers Scripts → Edit) and run:"));
1223
+ console.log(" " + C.cyan(`curl -X PUT "https://api.cloudflare.com/client/v4/accounts/${fix.accountId || "<ACCOUNT_ID>"}/workers/subdomain" -H "Authorization: Bearer <TU_TOKEN>" -H "Content-Type: application/json" -d '{"subdomain":"${fix.candidate || "kooni"}"'`));
1224
+ console.log(" " + m("Luego reintenta: npx kooni-bot deploy", "Then retry: npx kooni-bot deploy"));
1225
+ throw new Error(t().deployFailed);
1226
+ }
1227
+ } else {
1228
+ // Muestra el detalle real del deploy en vez de tragarlo: así el usuario ve
1229
+ // qué falló (deploy-check, binding, auth…) y puede corregirlo.
1230
+ const tail = errText.trim().split("\n").slice(-12).join("\n");
1231
+ if (tail) console.log(C.red(" ✗ " + tail));
1232
+ throw new Error(t().deployFailed);
1233
+ }
999
1234
  }
1000
1235
  if (url) {
1001
1236
  patchWranglerFile(dir, (s) => s.replace(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = "${url}"`));
@@ -1084,6 +1319,11 @@ Si dudas: **member/ es sagrado, src/ se actualiza.**
1084
1319
  3. Abrir el panel en \`https://<worker>.workers.dev/admin\` (usuario \`admin\` + \`DASHBOARD_PASSWORD\`).
1085
1320
  4. Conectar canales DESPUÉS del primer deploy (Telegram primero, ~5 min) desde \`/admin/conexiones\`.
1086
1321
 
1322
+ ## Subdominio workers.dev (error 10063)
1323
+ - Si el deploy falla con \`[code: 10063]\` ("You need a workers.dev subdomain"), el CLI **lo crea solo** usando la sesión OAuth de wrangler y reintenta el deploy automáticamente — no hay que hacer nada manual.
1324
+ - Si el CLI no pudo (sin sesión de Cloudflare), el usuario hace: dashboard → Workers & Pages → **Change** junto a "Your subdomain" → guardar. O crea un API token con permiso **Workers Scripts → Edit** en dash.cloudflare.com/profile/api-tokens y corre el curl que el propio CLI imprime.
1325
+ - El subdominio es ÚNICO por cuenta de Cloudflare (no por bot); se crea una sola vez.
1326
+
1087
1327
  ## Secrets y vars (referencia rápida)
1088
1328
  - Secrets: \`ANTHROPIC_API_KEY\` / \`OPENAI_API_KEY\` / \`XAI_API_KEY\` (cerebro), \`DASHBOARD_PASSWORD\` (panel), \`KB_REINDEX_TOKEN\` (reindex).
1089
1329
  - 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\`.
@@ -1202,9 +1442,13 @@ async function cmdInit(flags, rest) {
1202
1442
  kbName: meta && meta.kbName,
1203
1443
  });
1204
1444
 
1445
+ // Check-in al panel de licencias ANTES del deploy: así TODAS las instalaciones
1446
+ // (gratis o pagas) quedan registradas aunque el deploy falle (ej. error 10063).
1447
+ await checkin(dir, answers, version);
1448
+
1205
1449
  // deploy (si no lo deshabilitan)
1206
1450
  if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
1207
- const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
1451
+ const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "", _licenseCode: answers.licenseCode || "" };
1208
1452
  const url = await deployBot(dir, { flags: deployFlags, rl });
1209
1453
  if (url) {
1210
1454
  console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
@@ -1252,6 +1496,8 @@ async function cmdDeploy(flags, rest) {
1252
1496
  } catch {}
1253
1497
  flags.brainKey = brainKey;
1254
1498
  await deployBot(dir, { flags, rl });
1499
+ // Re-registrar tras un deploy exitoso (URL real del worker + tier/provider).
1500
+ try { await checkin(dir, {}, readPkgVersion(dir) || "0.0.0"); } catch {}
1255
1501
  } catch (e) {
1256
1502
  console.log("\n " + C.red("✗ " + (e.message || e)) + "\n");
1257
1503
  process.exit(1);
@@ -1448,11 +1694,13 @@ ${C.cyan("kooni-bot")} — ${t().helpIntro}
1448
1694
  ${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
1449
1695
  ${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
1450
1696
 
1697
+ ${C.dim(" Subdominio workers.dev: si tu cuenta no lo tiene, el deploy lo crea solo y reintenta.")}
1698
+
1451
1699
  ${C.dim(" Flags de init (modo no-interactivo, para agentes):")}
1452
1700
  ${C.dim(" --yes --slug <slug> --negocio <nombre> --bot-name <nombre> --lang es-MX|es-ES|en|pt-BR")}
1453
1701
  ${C.dim(" --tier free|pro --cerebro claude|chatgpt|grok|gateway --base-url <url>")}
1454
1702
  ${C.dim(" --que --ofrece --horario --ubicacion --telefono --web --pagos --faq --reglas --tono")}
1455
- ${C.dim(" --no-deploy --no-agent-skill --email <correo>")}
1703
+ ${C.dim(" --no-deploy --no-agent-skill --email <correo> --license <codigo-KOONI-PRO>")}
1456
1704
  `);
1457
1705
  }
1458
1706
 
@@ -1482,4 +1730,4 @@ if (IS_MAIN) {
1482
1730
  });
1483
1731
  }
1484
1732
 
1485
- export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers };
1733
+ export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers, cfOAuthAccessToken, ensureWorkersDevSubdomain };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kooni-bot",
3
- "version": "0.2.13",
3
+ "version": "0.2.16",
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",