kooni-bot 0.2.11 → 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 +287 -17
- 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
|
},
|
|
@@ -219,6 +232,7 @@ function normBotLang(v) {
|
|
|
219
232
|
const BRAINS = {
|
|
220
233
|
claude: { provider: "anthropic", secret: "ANTHROPIC_API_KEY", label: "Claude" },
|
|
221
234
|
chatgpt: { provider: "openai", secret: "OPENAI_API_KEY", label: "ChatGPT" },
|
|
235
|
+
aisa: { provider: "openai", secret: "OPENAI_API_KEY", label: "AIsa" },
|
|
222
236
|
grok: { provider: "xai", secret: "XAI_API_KEY", label: "Grok" },
|
|
223
237
|
minimax: { provider: "minimax", secret: "MINIMAX_API_KEY", label: "MiniMax" },
|
|
224
238
|
gateway: { provider: "openai", secret: "OPENAI_API_KEY", label: "Gateway" },
|
|
@@ -737,12 +751,15 @@ function writeDevVars(dir, answers, kbToken) {
|
|
|
737
751
|
lines.push("# " + answers.secret + "=<tu-api-key> ← para wrangler dev local, pégalo aquí (o usa el panel)");
|
|
738
752
|
lines.push(`DASHBOARD_PASSWORD=${answers.dashPassword || "kooni-local-password"}`);
|
|
739
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}`);
|
|
740
757
|
writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
|
|
741
758
|
}
|
|
742
759
|
|
|
743
760
|
function collectAnswers(flags) {
|
|
744
761
|
const rawBrain = String(flags.cerebro || flags.brain || "").trim().toLowerCase();
|
|
745
|
-
const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", minimax: "minimax", gateway: "gateway" })[rawBrain] || null;
|
|
762
|
+
const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", aisa: "aisa", grok: "grok", xai: "grok", minimax: "minimax", gateway: "gateway" })[rawBrain] || null;
|
|
746
763
|
const tone = ({ cercano: "cercano", friendly: "cercano", formal: "formal", divertido: "divertido", playful: "divertido" })[String(flags.tono || "").trim().toLowerCase()] || null;
|
|
747
764
|
|
|
748
765
|
// Lo que vino por flag se conserva; lo que NO vino queda undefined para que
|
|
@@ -770,6 +787,7 @@ function collectAnswers(flags) {
|
|
|
770
787
|
reglas: String(flags.reglas || "").trim() || undefined,
|
|
771
788
|
tone,
|
|
772
789
|
email: String(flags.email || "").trim() || undefined,
|
|
790
|
+
licenseCode: String(flags.license || "").trim() || undefined,
|
|
773
791
|
};
|
|
774
792
|
}
|
|
775
793
|
|
|
@@ -794,10 +812,13 @@ async function onboarding(rl, answers, defaultDir) {
|
|
|
794
812
|
// hace después desde el dashboard. Aquí solo fijamos el default.
|
|
795
813
|
answers.tier = answers.tier || "free";
|
|
796
814
|
|
|
797
|
-
const brainKeys = ["claude", "chatgpt", "grok", "minimax", "gateway"];
|
|
815
|
+
const brainKeys = ["claude", "chatgpt", "aisa", "grok", "minimax", "gateway"];
|
|
798
816
|
const brainIdx = await select(rl, t().brainQ, brainKeys.map((k) => ({
|
|
799
817
|
key: k, label: BRAINS[k].label,
|
|
800
|
-
desc: k === "claude" ? t().brainDesc
|
|
818
|
+
desc: k === "claude" ? t().brainDesc
|
|
819
|
+
: k === "chatgpt" ? "key directa de OpenAI (sk-proj-…)"
|
|
820
|
+
: k === "aisa" ? "key de AIsa (sk-ais…) · gateway api.aisa.one/v1"
|
|
821
|
+
: k === "gateway" ? "OpenRouter / cualquier gateway" : "",
|
|
801
822
|
})), { value: answers.brainKey, default: 0 });
|
|
802
823
|
answers.brainKey = brainKeys[brainIdx] || "claude";
|
|
803
824
|
answers.provider = BRAINS[answers.brainKey].provider;
|
|
@@ -805,6 +826,10 @@ async function onboarding(rl, answers, defaultDir) {
|
|
|
805
826
|
if (answers.brainKey === "gateway" && !answers.baseUrl) {
|
|
806
827
|
answers.baseUrl = await ask(rl, t().qBaseUrl, undefined) || "https://api.aisa.one/v1";
|
|
807
828
|
}
|
|
829
|
+
if (answers.brainKey === "aisa" && !answers.baseUrl) {
|
|
830
|
+
answers.baseUrl = "https://api.aisa.one/v1";
|
|
831
|
+
console.log(" " + C.yellow("ℹ️ AIsa configurado con OPENAI_API_BASE_URL=https://api.aisa.one/v1."));
|
|
832
|
+
}
|
|
808
833
|
|
|
809
834
|
// La API key se pide aquí, apenas se elige el cerebro — y se conserva para el
|
|
810
835
|
// deploy (se guarda como secret sin mostrarla). En no-interactivo se salta.
|
|
@@ -812,6 +837,20 @@ async function onboarding(rl, answers, defaultDir) {
|
|
|
812
837
|
answers.apiKey = await promptSecret(rl, t().qApiKey(BRAINS[answers.brainKey].label));
|
|
813
838
|
}
|
|
814
839
|
|
|
840
|
+
// Guard de gateway: si la key es de AIsa (o un gateway) y NO hay base URL, el
|
|
841
|
+
// bot desplegaría pero contestaría "Algo falló de mi lado" (401 contra OpenAI
|
|
842
|
+
// directo). Detectamos la key y la arreglamos o avisamos en el acto.
|
|
843
|
+
if (answers.provider === "openai" && !answers.baseUrl && answers.apiKey) {
|
|
844
|
+
const raw = String(answers.apiKey).trim();
|
|
845
|
+
const k = raw.toLowerCase();
|
|
846
|
+
if (k.startsWith("sk-ais") || k.includes("aisa")) {
|
|
847
|
+
answers.baseUrl = "https://api.aisa.one/v1";
|
|
848
|
+
console.log(" " + C.yellow("ℹ️ Detecté una key de AIsa — configuré OPENAI_API_BASE_URL=https://api.aisa.one/v1 automáticamente."));
|
|
849
|
+
} else if (!/^sk-proj-/.test(raw) && !/^sk-[A-Za-z0-9]{48}$/.test(raw)) {
|
|
850
|
+
console.warn(" " + C.yellow("⚠️ Tu key de OpenAI no parece directa (sk-proj-…). Si es de un gateway (AIsa/OpenRouter), el bot NO contestará sin OPENAI_API_BASE_URL en wrangler.toml — agrega el var o repite init eligiendo 'Gateway'."));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
815
854
|
answers.what = answers.what ?? (await ask(rl, t().qWhat, undefined) || "");
|
|
816
855
|
answers.offer = answers.offer ?? (await ask(rl, t().qOffer, undefined) || "");
|
|
817
856
|
answers.hours = answers.hours ?? (await ask(rl, t().qHours, undefined) || "");
|
|
@@ -829,9 +868,60 @@ async function onboarding(rl, answers, defaultDir) {
|
|
|
829
868
|
], { value: answers.tone, default: 0 });
|
|
830
869
|
answers.tone = ["cercano", "formal", "divertido"][toneIdx] || "cercano";
|
|
831
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
|
+
|
|
832
900
|
return answers;
|
|
833
901
|
}
|
|
834
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
|
+
|
|
835
925
|
// ── deploy ───────────────────────────────────────────────────────────────────
|
|
836
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;
|
|
837
927
|
|
|
@@ -863,6 +953,114 @@ function patchWranglerFile(dir, fn) {
|
|
|
863
953
|
writeFileSync(wt, fn(s));
|
|
864
954
|
}
|
|
865
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
|
+
|
|
866
1064
|
async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
867
1065
|
const wt = join(dir, "wrangler.toml");
|
|
868
1066
|
if (!existsSync(wt)) throw new Error(m("no encuentro wrangler.toml en " + dir, "can't find wrangler.toml in " + dir));
|
|
@@ -952,6 +1150,33 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
952
1150
|
}
|
|
953
1151
|
} catch {}
|
|
954
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
|
+
|
|
955
1180
|
// dependencias + migraciones + deploy (progreso visible en vivo)
|
|
956
1181
|
console.log("\n " + C.dim(t().installing));
|
|
957
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).")));
|
|
@@ -963,17 +1188,49 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
963
1188
|
console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
|
|
964
1189
|
|
|
965
1190
|
console.log(" " + C.dim(t().deploying));
|
|
966
|
-
|
|
967
|
-
try {
|
|
1191
|
+
const deployOnce = () => {
|
|
968
1192
|
const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
|
|
969
1193
|
// La URL real incluye el subdominio de la cuenta: <worker>.<cuenta>.workers.dev
|
|
970
|
-
|
|
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();
|
|
971
1201
|
} catch (e) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
+
}
|
|
977
1234
|
}
|
|
978
1235
|
if (url) {
|
|
979
1236
|
patchWranglerFile(dir, (s) => s.replace(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = "${url}"`));
|
|
@@ -1062,6 +1319,11 @@ Si dudas: **member/ es sagrado, src/ se actualiza.**
|
|
|
1062
1319
|
3. Abrir el panel en \`https://<worker>.workers.dev/admin\` (usuario \`admin\` + \`DASHBOARD_PASSWORD\`).
|
|
1063
1320
|
4. Conectar canales DESPUÉS del primer deploy (Telegram primero, ~5 min) desde \`/admin/conexiones\`.
|
|
1064
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
|
+
|
|
1065
1327
|
## Secrets y vars (referencia rápida)
|
|
1066
1328
|
- Secrets: \`ANTHROPIC_API_KEY\` / \`OPENAI_API_KEY\` / \`XAI_API_KEY\` (cerebro), \`DASHBOARD_PASSWORD\` (panel), \`KB_REINDEX_TOKEN\` (reindex).
|
|
1067
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\`.
|
|
@@ -1180,9 +1442,13 @@ async function cmdInit(flags, rest) {
|
|
|
1180
1442
|
kbName: meta && meta.kbName,
|
|
1181
1443
|
});
|
|
1182
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
|
+
|
|
1183
1449
|
// deploy (si no lo deshabilitan)
|
|
1184
1450
|
if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
|
|
1185
|
-
const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
|
|
1451
|
+
const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "", _licenseCode: answers.licenseCode || "" };
|
|
1186
1452
|
const url = await deployBot(dir, { flags: deployFlags, rl });
|
|
1187
1453
|
if (url) {
|
|
1188
1454
|
console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
|
|
@@ -1226,10 +1492,12 @@ async function cmdDeploy(flags, rest) {
|
|
|
1226
1492
|
try {
|
|
1227
1493
|
const wt = readFileSync(join(dir, "wrangler.toml"), "utf8");
|
|
1228
1494
|
const p = (wt.match(/LLM_PROVIDER\s*=\s*"([^"]+)"/) || [])[1] || "anthropic";
|
|
1229
|
-
brainKey = ({ anthropic: "claude", openai: "chatgpt", xai: "grok", minimax: "minimax" })[p] || "claude";
|
|
1495
|
+
brainKey = ({ anthropic: "claude", openai: "chatgpt", aisa: "aisa", xai: "grok", minimax: "minimax" })[p] || "claude";
|
|
1230
1496
|
} catch {}
|
|
1231
1497
|
flags.brainKey = brainKey;
|
|
1232
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 {}
|
|
1233
1501
|
} catch (e) {
|
|
1234
1502
|
console.log("\n " + C.red("✗ " + (e.message || e)) + "\n");
|
|
1235
1503
|
process.exit(1);
|
|
@@ -1426,11 +1694,13 @@ ${C.cyan("kooni-bot")} — ${t().helpIntro}
|
|
|
1426
1694
|
${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
|
|
1427
1695
|
${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
|
|
1428
1696
|
|
|
1697
|
+
${C.dim(" Subdominio workers.dev: si tu cuenta no lo tiene, el deploy lo crea solo y reintenta.")}
|
|
1698
|
+
|
|
1429
1699
|
${C.dim(" Flags de init (modo no-interactivo, para agentes):")}
|
|
1430
1700
|
${C.dim(" --yes --slug <slug> --negocio <nombre> --bot-name <nombre> --lang es-MX|es-ES|en|pt-BR")}
|
|
1431
1701
|
${C.dim(" --tier free|pro --cerebro claude|chatgpt|grok|gateway --base-url <url>")}
|
|
1432
1702
|
${C.dim(" --que --ofrece --horario --ubicacion --telefono --web --pagos --faq --reglas --tono")}
|
|
1433
|
-
${C.dim(" --no-deploy --no-agent-skill --email <correo>")}
|
|
1703
|
+
${C.dim(" --no-deploy --no-agent-skill --email <correo> --license <codigo-KOONI-PRO>")}
|
|
1434
1704
|
`);
|
|
1435
1705
|
}
|
|
1436
1706
|
|
|
@@ -1460,4 +1730,4 @@ if (IS_MAIN) {
|
|
|
1460
1730
|
});
|
|
1461
1731
|
}
|
|
1462
1732
|
|
|
1463
|
-
export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers };
|
|
1733
|
+
export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers, cfOAuthAccessToken, ensureWorkersDevSubdomain };
|
package/package.json
CHANGED