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.
- package/bin/kooni.js +261 -13
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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