kooni-bot 0.2.13 → 0.3.0
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 +277 -14
- 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, createPublicKey, verify as edVerify } from "node:crypto";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
|
|
25
|
-
const CLI_VERSION = "0.2.
|
|
25
|
+
const CLI_VERSION = "0.2.17";
|
|
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,21 @@ 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
|
+
// Clave PÚBLICA de licencias (Ed25519). Verifica códigos KOONI-PRO-V2-… pero NO
|
|
41
|
+
// puede firmarlos: es segura de publicar en npm — ese es el punto de la v2.
|
|
42
|
+
// DEBE ser la misma que está embebida en src/license.ts del bot. La PRIVADA vive
|
|
43
|
+
// solo en el panel de licencias (InsForge, secret LICENSE_PRIVATE_KEY).
|
|
44
|
+
//
|
|
45
|
+
// v2 (2026-09-01): reemplaza a LICENSE_MASTER_KEY (HMAC), que viajaba en este
|
|
46
|
+
// mismo archivo público y permitía falsificar licencias — hallazgo S2 del PLAN.
|
|
47
|
+
const LICENSE_PUBLIC_KEY = process.env.KOONI_LICENSE_PUBLIC_KEY || "MCowBQYDK2VwAyEAqpP9OBrju8ebMWjQM4uYLsUV5yqWG8k8ieozT8Me8EQ=";
|
|
48
|
+
|
|
49
|
+
// Token compartido de registro/uso (X-Kooni-Token). DEBE coincidir con el secret
|
|
50
|
+
// REGISTER_TOKEN del panel de licencias (InsForge). Es "seguridad por oscuridad"
|
|
51
|
+
// (vive en el CLI público y en cada worker — ver tarea S4 en PLAN.md): el blindaje
|
|
52
|
+
// real lo dan la validación de formato y el rechazo de uids desconocidos.
|
|
53
|
+
const KOONI_REGISTER_TOKEN = process.env.KOONI_REGISTER_TOKEN || "1f8740b841b71f21eba510eb12b8e6870b5949a916cf1162";
|
|
54
|
+
|
|
40
55
|
// ── color ─────────────────────────────────────────────────────────────────────
|
|
41
56
|
const C = {
|
|
42
57
|
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
@@ -122,6 +137,9 @@ const DICT = {
|
|
|
122
137
|
// error
|
|
123
138
|
templateMissing: "no encontré el instalador en el template.",
|
|
124
139
|
deployFailed: "el deploy falló. Revisa los mensajes de arriba.",
|
|
140
|
+
noSubdomain: "tu cuenta de Cloudflare no tiene subdominio workers.dev — lo estoy creando automáticamente…",
|
|
141
|
+
subdomainOk: (s) => `subdominio listo: ${s}.workers.dev`,
|
|
142
|
+
subdomainManual: "no pude crear el subdominio automáticamente (no encontré tu sesión de Cloudflare). Hazlo manual, 1 min:",
|
|
125
143
|
unknown: "comando desconocido:",
|
|
126
144
|
helpIntro: "instala tu asistente de IA en Cloudflare",
|
|
127
145
|
},
|
|
@@ -189,6 +207,9 @@ const DICT = {
|
|
|
189
207
|
qEmail: "Your email? (optional, for support)",
|
|
190
208
|
templateMissing: "installer not found in template.",
|
|
191
209
|
deployFailed: "deploy failed. Check the messages above.",
|
|
210
|
+
noSubdomain: "your Cloudflare account has no workers.dev subdomain yet — creating it automatically…",
|
|
211
|
+
subdomainOk: (s) => `subdomain ready: ${s}.workers.dev`,
|
|
212
|
+
subdomainManual: "couldn't create the subdomain automatically (I couldn't find your Cloudflare session). Do it manually, 1 min:",
|
|
192
213
|
unknown: "unknown command:",
|
|
193
214
|
helpIntro: "install your AI assistant on Cloudflare",
|
|
194
215
|
},
|
|
@@ -651,6 +672,13 @@ function stampWrangler(dir, answers, botUid) {
|
|
|
651
672
|
s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1BOT_INSTANCE_ID = "${uid}"\n`);
|
|
652
673
|
}
|
|
653
674
|
|
|
675
|
+
// Token compartido de registro/uso (panel de licencias del dueño).
|
|
676
|
+
if (hasInVars("KOONI_REGISTER_TOKEN")) {
|
|
677
|
+
set(/KOONI_REGISTER_TOKEN\s*=\s*"[^"]*"/g, `KOONI_REGISTER_TOKEN = "${KOONI_REGISTER_TOKEN}"`);
|
|
678
|
+
} else {
|
|
679
|
+
s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1KOONI_REGISTER_TOKEN = "${KOONI_REGISTER_TOKEN}"\n`);
|
|
680
|
+
}
|
|
681
|
+
|
|
654
682
|
if (answers.provider !== "anthropic") {
|
|
655
683
|
if (hasInVars("LLM_PROVIDER")) {
|
|
656
684
|
set(/LLM_PROVIDER\s*=\s*"[^"]*"/g, `LLM_PROVIDER = "${answers.provider}"`);
|
|
@@ -738,6 +766,10 @@ function writeDevVars(dir, answers, kbToken) {
|
|
|
738
766
|
lines.push("# " + answers.secret + "=<tu-api-key> ← para wrangler dev local, pégalo aquí (o usa el panel)");
|
|
739
767
|
lines.push(`DASHBOARD_PASSWORD=${answers.dashPassword || "kooni-local-password"}`);
|
|
740
768
|
lines.push(`KB_REINDEX_TOKEN=${kbToken}`);
|
|
769
|
+
// (v2) Ya NO se escribe ninguna llave de licencias: el bot verifica con la
|
|
770
|
+
// clave pública que trae embebida y no necesita ningún secret para eso.
|
|
771
|
+
// Token compartido de registro/uso (X-Kooni-Token para el panel de licencias).
|
|
772
|
+
lines.push(`KOONI_REGISTER_TOKEN=${KOONI_REGISTER_TOKEN}`);
|
|
741
773
|
writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
|
|
742
774
|
}
|
|
743
775
|
|
|
@@ -771,6 +803,7 @@ function collectAnswers(flags) {
|
|
|
771
803
|
reglas: String(flags.reglas || "").trim() || undefined,
|
|
772
804
|
tone,
|
|
773
805
|
email: String(flags.email || "").trim() || undefined,
|
|
806
|
+
licenseCode: String(flags.license || "").trim() || undefined,
|
|
774
807
|
};
|
|
775
808
|
}
|
|
776
809
|
|
|
@@ -851,9 +884,65 @@ async function onboarding(rl, answers, defaultDir) {
|
|
|
851
884
|
], { value: answers.tone, default: 0 });
|
|
852
885
|
answers.tone = ["cercano", "formal", "divertido"][toneIdx] || "cercano";
|
|
853
886
|
|
|
887
|
+
// Correo del dueño: se pide SIEMPRE (registra la instalación en el panel de
|
|
888
|
+
// licencias y es el canal de contacto/renovación). En interactivo se exige un
|
|
889
|
+
// valor (con reintento); en modo agente/CI viene por --email.
|
|
890
|
+
if (!answers.email) {
|
|
891
|
+
while (interactive()) {
|
|
892
|
+
const em = (await ask(rl, t().qEmail, undefined)).trim();
|
|
893
|
+
if (em) { answers.email = em; break; }
|
|
894
|
+
console.log(" " + C.yellow("⚠") + " " + m("el correo es obligatorio para registrar tu instalación.", "email is required to register your install."));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Licencia Pro: si el usuario dice que es Pro, pide el código y lo valida
|
|
899
|
+
// localmente (HMAC con la master key). Si es válido se guarda tras el deploy
|
|
900
|
+
// (settings pro_license → límites quitados). Si es inválido, se avisa y sigue gratis.
|
|
901
|
+
const wantsPro = interactive()
|
|
902
|
+
? await confirm(rl, m("¿Tu bot será Pro (tienes una licencia)?", "Will your bot be Pro (do you have a license)?"))
|
|
903
|
+
: !!(flags.license || answers.licenseCode);
|
|
904
|
+
if (wantsPro) {
|
|
905
|
+
const code = answers.licenseCode || (interactive()
|
|
906
|
+
? await ask(rl, m("Pega tu código KOONI-PRO-V2-…:", "Paste your KOONI-PRO-V2-… code:"), undefined)
|
|
907
|
+
: String(flags.license || "").trim());
|
|
908
|
+
if (code && verifyKooniLicense(code)) {
|
|
909
|
+
answers.licenseCode = code.trim();
|
|
910
|
+
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."));
|
|
911
|
+
} else if (code) {
|
|
912
|
+
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."));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
854
916
|
return answers;
|
|
855
917
|
}
|
|
856
918
|
|
|
919
|
+
// ── licencia Pro ─────────────────────────────────────────────────────────────
|
|
920
|
+
// Valida un código KOONI-PRO-V2-… localmente (mismo formato que src/license.ts
|
|
921
|
+
// del bot): firma Ed25519 verificada con la clave PÚBLICA. No exige que esté
|
|
922
|
+
// ligado a esta instalación (eso lo revalida el panel al leerlo).
|
|
923
|
+
// Sin red: la verificación es puramente local, igual que antes.
|
|
924
|
+
function verifyKooniLicense(code) {
|
|
925
|
+
const trimmed = String(code || "").trim();
|
|
926
|
+
const prefix = "KOONI-PRO-V2-";
|
|
927
|
+
if (!trimmed.startsWith(prefix)) return false;
|
|
928
|
+
const rest = trimmed.slice(prefix.length);
|
|
929
|
+
const dot = rest.lastIndexOf(".");
|
|
930
|
+
if (dot <= 0) return false;
|
|
931
|
+
const enc = rest.slice(0, dot);
|
|
932
|
+
const sig = rest.slice(dot + 1);
|
|
933
|
+
try {
|
|
934
|
+
const pub = createPublicKey({
|
|
935
|
+
key: Buffer.from(LICENSE_PUBLIC_KEY, "base64"),
|
|
936
|
+
format: "der",
|
|
937
|
+
type: "spki",
|
|
938
|
+
});
|
|
939
|
+
if (!edVerify(null, Buffer.from(enc, "utf8"), pub, Buffer.from(sig, "hex"))) return false;
|
|
940
|
+
const payload = JSON.parse(Buffer.from(enc, "base64url").toString("utf8"));
|
|
941
|
+
if (payload.kind === "monthly" && payload.expiry && Date.now() > payload.expiry) return false;
|
|
942
|
+
return true;
|
|
943
|
+
} catch { return false; }
|
|
944
|
+
}
|
|
945
|
+
|
|
857
946
|
// ── deploy ───────────────────────────────────────────────────────────────────
|
|
858
947
|
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
948
|
|
|
@@ -885,6 +974,114 @@ function patchWranglerFile(dir, fn) {
|
|
|
885
974
|
writeFileSync(wt, fn(s));
|
|
886
975
|
}
|
|
887
976
|
|
|
977
|
+
// ── workers.dev subdomain (Cloudflare error 10063) ───────────────────────────
|
|
978
|
+
// Cuando la cuenta aún no tiene subdominio workers.dev, `wrangler deploy` falla
|
|
979
|
+
// con [code: 10063]. Lo resolvemos SOLO con la API de Cloudflare reutilizando la
|
|
980
|
+
// sesión OAuth que wrangler ya guardó en disco (sin pedirle nada al usuario); si
|
|
981
|
+
// no es posible, mostramos los pasos manuales + la opción de API token.
|
|
982
|
+
const CF_OAUTH_CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7"; // cliente OAuth público de wrangler
|
|
983
|
+
|
|
984
|
+
function wranglerConfigCandidates() {
|
|
985
|
+
const h = homedir();
|
|
986
|
+
const appdata = process.env.APPDATA || join(h, "AppData", "Roaming");
|
|
987
|
+
return [
|
|
988
|
+
join(appdata, "xdg.config", ".wrangler", "config", "default.toml"),
|
|
989
|
+
join(h, ".config", ".wrangler", "config", "default.toml"),
|
|
990
|
+
join(h, ".wrangler", "config", "default.toml"),
|
|
991
|
+
join(h, "AppData", "Local", ".wrangler", "config", "default.toml"),
|
|
992
|
+
].filter((p) => existsSync(p));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Access token OAuth temporal a partir de la sesión guardada de wrangler.
|
|
996
|
+
// IMPORTANTE: Cloudflare ROTA el refresh token en cada intercambio. Si no lo
|
|
997
|
+
// devolvemos al config, invalidamos la sesión de wrangler (error 9109 al
|
|
998
|
+
// siguiente deploy). Por eso: 1) si el access token almacenado aún no expiró,
|
|
999
|
+
// se usa directo (sin tocar el refresh); 2) si hay que refrescar, se persiste
|
|
1000
|
+
// el par nuevo de vuelta al config, igual que hace wrangler.
|
|
1001
|
+
async function cfOAuthAccessToken() {
|
|
1002
|
+
const cfgPath = wranglerConfigCandidates()[0];
|
|
1003
|
+
if (!cfgPath) return null;
|
|
1004
|
+
let raw = "";
|
|
1005
|
+
try { raw = readFileSync(cfgPath, "utf8"); } catch { return null; }
|
|
1006
|
+
const pick = (k) => {
|
|
1007
|
+
const line = raw.split(/\r?\n/).find((l) => l.trim().startsWith(k + " "));
|
|
1008
|
+
if (!line) return "";
|
|
1009
|
+
const m = line.match(/=\s*"([^"]*)"/);
|
|
1010
|
+
return m ? m[1] : "";
|
|
1011
|
+
};
|
|
1012
|
+
const storedAccess = pick("oauth_token");
|
|
1013
|
+
const exp = pick("expiration_time");
|
|
1014
|
+
// 1) Access token almacenado aún vigente → úsalo directo (cero rotación).
|
|
1015
|
+
if (storedAccess && exp && new Date(exp).getTime() > Date.now()) {
|
|
1016
|
+
try {
|
|
1017
|
+
const probe = await fetchRetry("https://api.cloudflare.com/client/v4/accounts", { headers: { Authorization: "Bearer " + storedAccess } }, { ms: 8000, tries: 1 });
|
|
1018
|
+
if (probe.ok) return storedAccess;
|
|
1019
|
+
} catch {}
|
|
1020
|
+
}
|
|
1021
|
+
// 2) Refrescar: intercambia el refresh token y PERSISTE el par nuevo.
|
|
1022
|
+
const refresh = pick("refresh_token") || pick("oauth_token");
|
|
1023
|
+
if (!refresh) return null;
|
|
1024
|
+
try {
|
|
1025
|
+
const res = await fetchRetry("https://dash.cloudflare.com/oauth2/token", {
|
|
1026
|
+
method: "POST",
|
|
1027
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1028
|
+
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refresh, client_id: CF_OAUTH_CLIENT_ID }),
|
|
1029
|
+
}, { ms: 10000, tries: 2 });
|
|
1030
|
+
if (!res.ok) return null;
|
|
1031
|
+
const j = await res.json().catch(() => ({}));
|
|
1032
|
+
if (!j.access_token) return null;
|
|
1033
|
+
try {
|
|
1034
|
+
const nextRefresh = j.refresh_token || refresh;
|
|
1035
|
+
const expIso = new Date(Date.now() + (j.expires_in || 3599) * 1000).toISOString();
|
|
1036
|
+
const out = raw
|
|
1037
|
+
.replace(/^oauth_token\s*=\s*"[^"]*"/m, `oauth_token = "${j.access_token}"`)
|
|
1038
|
+
.replace(/^refresh_token\s*=\s*"[^"]*"/m, `refresh_token = "${nextRefresh}"`)
|
|
1039
|
+
.replace(/^expiration_time\s*=\s*"[^"]*"/m, `expiration_time = "${expIso}"`);
|
|
1040
|
+
if (out !== raw) writeFileSync(cfgPath, out);
|
|
1041
|
+
} catch {}
|
|
1042
|
+
return j.access_token;
|
|
1043
|
+
} catch { return null; }
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function cfApi(access, path, method = "GET", body) {
|
|
1047
|
+
const res = await fetchRetry("https://api.cloudflare.com/client/v4" + path, {
|
|
1048
|
+
method,
|
|
1049
|
+
headers: { Authorization: "Bearer " + access, "Content-Type": "application/json" },
|
|
1050
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
1051
|
+
}, { ms: 15000, tries: 2 });
|
|
1052
|
+
const json = await res.json().catch(() => ({}));
|
|
1053
|
+
return { status: res.status, json };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Nombre candidato para el subdominio, derivado del slug del bot.
|
|
1057
|
+
function proposeSubdomain(slug) {
|
|
1058
|
+
const s = sanitizeSlug(slug || "kooni").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1059
|
+
return (s.slice(0, 24).replace(/-+$/, "") || "kooni");
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function ensureWorkersDevSubdomain(dir) {
|
|
1063
|
+
const slug = (readMarker(dir) || {}).slug || basename(dir);
|
|
1064
|
+
const access = await cfOAuthAccessToken();
|
|
1065
|
+
if (!access) return { ok: false, reason: "no-oauth" };
|
|
1066
|
+
try {
|
|
1067
|
+
const acc = await cfApi(access, "/accounts");
|
|
1068
|
+
const accounts = (acc.json && acc.json.result) || [];
|
|
1069
|
+
if (!accounts.length) return { ok: false, reason: "no-account" };
|
|
1070
|
+
const account = accounts[0];
|
|
1071
|
+
const sub = await cfApi(access, `/accounts/${account.id}/workers/subdomain`);
|
|
1072
|
+
const existing = sub.json && sub.json.result && sub.json.result.subdomain;
|
|
1073
|
+
if (existing) return { ok: true, subdomain: existing, accountId: account.id, accountName: account.name, via: "existing" };
|
|
1074
|
+
const candidate = proposeSubdomain(slug);
|
|
1075
|
+
const put = await cfApi(access, `/accounts/${account.id}/workers/subdomain`, "PUT", { subdomain: candidate });
|
|
1076
|
+
const created = put.json && put.json.result && put.json.result.subdomain;
|
|
1077
|
+
if (put.status < 300 && created) return { ok: true, subdomain: created, accountId: account.id, accountName: account.name, via: "created" };
|
|
1078
|
+
const msg = (put.json && put.json.errors && put.json.errors[0] && put.json.errors[0].message) || `HTTP ${put.status}`;
|
|
1079
|
+
return { ok: false, reason: "create-failed", message: msg, candidate, accountId: account.id };
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
return { ok: false, reason: "api-error", message: e.message || String(e) };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
888
1085
|
async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
889
1086
|
const wt = join(dir, "wrangler.toml");
|
|
890
1087
|
if (!existsSync(wt)) throw new Error(m("no encuentro wrangler.toml en " + dir, "can't find wrangler.toml in " + dir));
|
|
@@ -974,6 +1171,27 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
974
1171
|
}
|
|
975
1172
|
} catch {}
|
|
976
1173
|
|
|
1174
|
+
// Licencia Pro capturada en el onboarding: se activa después del deploy
|
|
1175
|
+
// escribiendo el código en settings (key pro_license), igual que el panel.
|
|
1176
|
+
const licenseCode = (flags.license || "") ? String(flags.license).trim() : (flags._licenseCode || "");
|
|
1177
|
+
if (flags._licenseCode) {
|
|
1178
|
+
try {
|
|
1179
|
+
const code = String(flags._licenseCode).trim();
|
|
1180
|
+
const now = Date.now();
|
|
1181
|
+
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;`;
|
|
1182
|
+
const tmp = join(dir, ".kooni-license.sql");
|
|
1183
|
+
writeFileSync(tmp, sql);
|
|
1184
|
+
wrangler(dir, ["d1", "execute", dbName, "--file=" + tmp, "--remote"], { capture: true });
|
|
1185
|
+
rmSync(tmp, { force: true });
|
|
1186
|
+
console.log(" " + C.green("✓") + " " + m("licencia Pro activada (código guardado)", "Pro license activated (code saved)"));
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
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."));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// (v2) No hay secret de licencias que instalar: el worker verifica los códigos
|
|
1193
|
+
// con la clave PÚBLICA embebida en su propio código. Una llave menos viajando.
|
|
1194
|
+
|
|
977
1195
|
// dependencias + migraciones + deploy (progreso visible en vivo)
|
|
978
1196
|
console.log("\n " + C.dim(t().installing));
|
|
979
1197
|
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 +1203,49 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
985
1203
|
console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
|
|
986
1204
|
|
|
987
1205
|
console.log(" " + C.dim(t().deploying));
|
|
988
|
-
|
|
989
|
-
try {
|
|
1206
|
+
const deployOnce = () => {
|
|
990
1207
|
const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
|
|
991
1208
|
// La URL real incluye el subdominio de la cuenta: <worker>.<cuenta>.workers.dev
|
|
992
|
-
|
|
1209
|
+
return (dep.match(/https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev/) || [])[0] || "";
|
|
1210
|
+
};
|
|
1211
|
+
const deployErrorText = (e) => ((e && (e.stderr || e.stdout || e.message)) || "").toString();
|
|
1212
|
+
|
|
1213
|
+
let url = "";
|
|
1214
|
+
try {
|
|
1215
|
+
url = deployOnce();
|
|
993
1216
|
} catch (e) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1217
|
+
const errText = deployErrorText(e);
|
|
1218
|
+
if (/10063|workers\.dev subdomain/.test(errText)) {
|
|
1219
|
+
// La cuenta no tiene subdominio workers.dev: lo creamos automáticamente
|
|
1220
|
+
// con la sesión OAuth de wrangler y reintentamos el deploy.
|
|
1221
|
+
console.log(" " + C.yellow("⚠") + " " + t().noSubdomain);
|
|
1222
|
+
const fix = await ensureWorkersDevSubdomain(dir);
|
|
1223
|
+
if (fix.ok) {
|
|
1224
|
+
console.log(" " + C.green("✓") + " " + t().subdomainOk(fix.subdomain));
|
|
1225
|
+
try {
|
|
1226
|
+
url = deployOnce();
|
|
1227
|
+
} catch (e2) {
|
|
1228
|
+
const tail2 = deployErrorText(e2).trim().split("\n").slice(-12).join("\n");
|
|
1229
|
+
if (tail2) console.log(C.red(" ✗ " + tail2));
|
|
1230
|
+
throw new Error(t().deployFailed);
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
// Fallback: instrucciones manuales + opción de API token.
|
|
1234
|
+
console.log(C.red(" ✗ " + t().subdomainManual));
|
|
1235
|
+
if (fix.message) console.log(" " + C.dim(fix.message));
|
|
1236
|
+
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'")));
|
|
1237
|
+
console.log(" " + m("o crea un API token (Workers Scripts → Edit) y corre:", "or create an API token (Workers Scripts → Edit) and run:"));
|
|
1238
|
+
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"}"'`));
|
|
1239
|
+
console.log(" " + m("Luego reintenta: npx kooni-bot deploy", "Then retry: npx kooni-bot deploy"));
|
|
1240
|
+
throw new Error(t().deployFailed);
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
// Muestra el detalle real del deploy en vez de tragarlo: así el usuario ve
|
|
1244
|
+
// qué falló (deploy-check, binding, auth…) y puede corregirlo.
|
|
1245
|
+
const tail = errText.trim().split("\n").slice(-12).join("\n");
|
|
1246
|
+
if (tail) console.log(C.red(" ✗ " + tail));
|
|
1247
|
+
throw new Error(t().deployFailed);
|
|
1248
|
+
}
|
|
999
1249
|
}
|
|
1000
1250
|
if (url) {
|
|
1001
1251
|
patchWranglerFile(dir, (s) => s.replace(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = "${url}"`));
|
|
@@ -1016,7 +1266,7 @@ async function checkin(dir, answers, version) {
|
|
|
1016
1266
|
const slug = answers.slug || marker.slug || basename(dir);
|
|
1017
1267
|
await fetchTimeout(CHECKIN_URL, {
|
|
1018
1268
|
method: "POST",
|
|
1019
|
-
headers: { "Content-Type": "application/json" },
|
|
1269
|
+
headers: { "Content-Type": "application/json", "X-Kooni-Token": KOONI_REGISTER_TOKEN },
|
|
1020
1270
|
body: JSON.stringify({
|
|
1021
1271
|
email: answers.email || undefined,
|
|
1022
1272
|
slug,
|
|
@@ -1084,6 +1334,11 @@ Si dudas: **member/ es sagrado, src/ se actualiza.**
|
|
|
1084
1334
|
3. Abrir el panel en \`https://<worker>.workers.dev/admin\` (usuario \`admin\` + \`DASHBOARD_PASSWORD\`).
|
|
1085
1335
|
4. Conectar canales DESPUÉS del primer deploy (Telegram primero, ~5 min) desde \`/admin/conexiones\`.
|
|
1086
1336
|
|
|
1337
|
+
## Subdominio workers.dev (error 10063)
|
|
1338
|
+
- 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.
|
|
1339
|
+
- 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.
|
|
1340
|
+
- El subdominio es ÚNICO por cuenta de Cloudflare (no por bot); se crea una sola vez.
|
|
1341
|
+
|
|
1087
1342
|
## Secrets y vars (referencia rápida)
|
|
1088
1343
|
- Secrets: \`ANTHROPIC_API_KEY\` / \`OPENAI_API_KEY\` / \`XAI_API_KEY\` (cerebro), \`DASHBOARD_PASSWORD\` (panel), \`KB_REINDEX_TOKEN\` (reindex).
|
|
1089
1344
|
- 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 +1457,13 @@ async function cmdInit(flags, rest) {
|
|
|
1202
1457
|
kbName: meta && meta.kbName,
|
|
1203
1458
|
});
|
|
1204
1459
|
|
|
1460
|
+
// Check-in al panel de licencias ANTES del deploy: así TODAS las instalaciones
|
|
1461
|
+
// (gratis o pagas) quedan registradas aunque el deploy falle (ej. error 10063).
|
|
1462
|
+
await checkin(dir, answers, version);
|
|
1463
|
+
|
|
1205
1464
|
// deploy (si no lo deshabilitan)
|
|
1206
1465
|
if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
|
|
1207
|
-
const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
|
|
1466
|
+
const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "", _licenseCode: answers.licenseCode || "" };
|
|
1208
1467
|
const url = await deployBot(dir, { flags: deployFlags, rl });
|
|
1209
1468
|
if (url) {
|
|
1210
1469
|
console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
|
|
@@ -1252,6 +1511,8 @@ async function cmdDeploy(flags, rest) {
|
|
|
1252
1511
|
} catch {}
|
|
1253
1512
|
flags.brainKey = brainKey;
|
|
1254
1513
|
await deployBot(dir, { flags, rl });
|
|
1514
|
+
// Re-registrar tras un deploy exitoso (URL real del worker + tier/provider).
|
|
1515
|
+
try { await checkin(dir, {}, readPkgVersion(dir) || "0.0.0"); } catch {}
|
|
1255
1516
|
} catch (e) {
|
|
1256
1517
|
console.log("\n " + C.red("✗ " + (e.message || e)) + "\n");
|
|
1257
1518
|
process.exit(1);
|
|
@@ -1448,11 +1709,13 @@ ${C.cyan("kooni-bot")} — ${t().helpIntro}
|
|
|
1448
1709
|
${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
|
|
1449
1710
|
${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
|
|
1450
1711
|
|
|
1712
|
+
${C.dim(" Subdominio workers.dev: si tu cuenta no lo tiene, el deploy lo crea solo y reintenta.")}
|
|
1713
|
+
|
|
1451
1714
|
${C.dim(" Flags de init (modo no-interactivo, para agentes):")}
|
|
1452
1715
|
${C.dim(" --yes --slug <slug> --negocio <nombre> --bot-name <nombre> --lang es-MX|es-ES|en|pt-BR")}
|
|
1453
1716
|
${C.dim(" --tier free|pro --cerebro claude|chatgpt|grok|gateway --base-url <url>")}
|
|
1454
1717
|
${C.dim(" --que --ofrece --horario --ubicacion --telefono --web --pagos --faq --reglas --tono")}
|
|
1455
|
-
${C.dim(" --no-deploy --no-agent-skill --email <correo>")}
|
|
1718
|
+
${C.dim(" --no-deploy --no-agent-skill --email <correo> --license <codigo-KOONI-PRO-V2>")}
|
|
1456
1719
|
`);
|
|
1457
1720
|
}
|
|
1458
1721
|
|
|
@@ -1482,4 +1745,4 @@ if (IS_MAIN) {
|
|
|
1482
1745
|
});
|
|
1483
1746
|
}
|
|
1484
1747
|
|
|
1485
|
-
export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers };
|
|
1748
|
+
export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers, cfOAuthAccessToken, ensureWorkersDevSubdomain };
|
package/package.json
CHANGED