boosterarena-mcp 0.3.3 → 0.3.4
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/LEEME.md +3 -1
- package/dist/nucleo.mjs +156 -24
- package/package.json +1 -1
- package/proyectos.mjs +6 -1
- package/servidor.mjs +161 -5
- package/sesion.mjs +91 -10
package/LEEME.md
CHANGED
|
@@ -50,7 +50,9 @@ habla con Booster con **tu** sesión.
|
|
|
50
50
|
## Qué puede hacer
|
|
51
51
|
|
|
52
52
|
**Proyectos** — `listar_proyectos`, `abrir_proyecto`, `crear_proyecto`, `guardar_proyecto`,
|
|
53
|
-
`ver_slide
|
|
53
|
+
`ver_slide`, `ir_a_slide` (volver a un slide ya hecho para corregirlo; `abrir_proyecto` también acepta
|
|
54
|
+
`escena` y `slide`) y `tanda` (muchas órdenes en una sola llamada: es la forma barata de armar un
|
|
55
|
+
slide). Nada se sube hasta que se llama a `guardar_proyecto`: el agente trabaja sobre un
|
|
54
56
|
borrador y tú decides cuándo se publica.
|
|
55
57
|
|
|
56
58
|
**Contenido** — poner texto, imágenes, figuras y dibujos de la biblioteca (más de 3.000
|
package/dist/nucleo.mjs
CHANGED
|
@@ -1587,7 +1587,7 @@ function construirCatalogo(opts) {
|
|
|
1587
1587
|
segundos: num('Solo con evento "time": a los cu\xE1ntos segundos.'),
|
|
1588
1588
|
paso: num('Solo con evento "clickStep": en qu\xE9 n\xFAmero de clic (1, 2, 3...).'),
|
|
1589
1589
|
accion: str(
|
|
1590
|
-
"QU\xC9 pasa. abrirCapa = muestra una capa \xB7 irAEscena = salta a otra escena \xB7 irASlide = salta a un slide \xB7 cerrarCapa = cierra la capa abierta \xB7 leerEnVoz = lo lee el navegador en voz alta \xB7 abrirEnlace = abre una URL \xB7 ocultar = el objeto desaparece con una nube de humo \xB7 moverPorTrayectoria = el objeto RECORRE un camino dibujado \xB7 sonar = suena un audio que ya est\xE1 puesto en el slide o en una capa (para \xABque suene al abrirse la capa\xBB: objetivo = ese audio, evento = layerShown, destino = ese mismo audio; para \xABal tocar X\xBB: objetivo = X, destino = el audio) \xB7 reiniciarSecuencia = devuelve el slide a como empez\xF3 \xB7 quitar = le borra todo lo que tuviera programado.",
|
|
1590
|
+
"QU\xC9 pasa. abrirCapa = muestra una capa \xB7 irAEscena = salta a otra escena \xB7 irASlide = salta a un slide \xB7 cerrarCapa = cierra la capa abierta \xB7 leerEnVoz = lo lee el navegador en voz alta \xB7 abrirEnlace = abre una URL \xB7 ocultar = el objeto desaparece con una nube de humo \xB7 moverPorTrayectoria = el objeto RECORRE un camino dibujado \xB7 sonar = suena un audio que ya est\xE1 puesto en el slide o en una capa (para \xABque suene al abrirse la capa\xBB: objetivo = ese audio, evento = layerShown, destino = ese mismo audio; para \xABal tocar X\xBB: objetivo = X, destino = el audio) \xB7 pausarAudio / detenerAudio = pausa o detiene (vuelve al principio) ese audio; destino = el audio, vac\xEDo = el que est\xE9 sonando \xB7 reiniciarSecuencia = devuelve el slide a como empez\xF3 \xB7 quitar = le borra todo lo que tuviera programado.",
|
|
1591
1591
|
{
|
|
1592
1592
|
enum: [
|
|
1593
1593
|
"abrirCapa",
|
|
@@ -1599,6 +1599,8 @@ function construirCatalogo(opts) {
|
|
|
1599
1599
|
"ocultar",
|
|
1600
1600
|
"moverPorTrayectoria",
|
|
1601
1601
|
"sonar",
|
|
1602
|
+
"pausarAudio",
|
|
1603
|
+
"detenerAudio",
|
|
1602
1604
|
"reiniciarSecuencia",
|
|
1603
1605
|
"quitar"
|
|
1604
1606
|
]
|
|
@@ -1694,7 +1696,10 @@ function construirCatalogo(opts) {
|
|
|
1694
1696
|
var PUNTO = obj({ x: num("En % del ancho del slide."), y: num("En % del alto del slide.") }, ["x", "y"]);
|
|
1695
1697
|
function conLoDelMcp(lista2, fn) {
|
|
1696
1698
|
const props = (nombre) => (lista2.find((t) => t.function.name === nombre)?.function.parameters).properties;
|
|
1699
|
+
const PEDIDO = str('Solo para `borrar` o accion "quitar": lo que te escribi\xF3 la persona pidi\xE9ndolo, COPIADO TAL CUAL (ej: "quita la programaci\xF3n del caldero 3"). Sin una petici\xF3n suya de borrar o quitar, no se borra nada. Nunca lo inventes ni lo escribas t\xFA.');
|
|
1700
|
+
props("borrar").pedido_del_maestro = PEDIDO;
|
|
1697
1701
|
const programar = props("programar");
|
|
1702
|
+
programar.pedido_del_maestro = PEDIDO;
|
|
1698
1703
|
programar.trayectoria = str('Con "moverPorTrayectoria" o el evento "trayectoriaTerminada": el NOMBRE del camino ("Trayectoria 1", "a la boca"). Se busca por nombre EXACTO. Un objeto puede tener varios: dale a cada uno el suyo y as\xED cada `clickStep` o cada `trayectoriaTerminada` apunta al que toca. Si el camino no existe todav\xEDa, hace falta `hasta` o `puntos` para trazarlo.');
|
|
1699
1704
|
programar.hasta = { ...PUNTO, description: 'Solo con "moverPorTrayectoria": a D\xD3NDE llega el CENTRO del objeto, en % del slide. El camino se traza en recta desde donde est\xE9 el objeto \u2014 o, en un `clickStep` 2, 3\u2026, desde donde lo dej\xF3 el paso anterior. Si ya existe un camino con ese `trayectoria`, se retraza.' };
|
|
1700
1705
|
programar.puntos = { type: "array", items: PUNTO, description: 'Solo con "moverPorTrayectoria": puntos por los que PASA el centro del objeto, en % del slide, para una curva o un recorrido con escalas. El \xFAltimo es la llegada (o, si mandas tambi\xE9n `hasta`, `hasta` va detr\xE1s).' };
|
|
@@ -1703,6 +1708,10 @@ function conLoDelMcp(lista2, fn) {
|
|
|
1703
1708
|
fondo.url = str("Una imagen ya SUBIDA (con `subir_imagen`), https://. Usa esto, `busca` o `color`: uno solo.");
|
|
1704
1709
|
fondo.capa = str("El `ref` o el nombre de una CAPA para ponerle el fondo a ella en vez de al slide. Cada capa puede llevar el suyo.");
|
|
1705
1710
|
props("nueva_capa").fondo = str("Fondo propio de la capa: la URL https:// de una imagen ya subida o un color en hex. Sin esto, la capa deja ver el slide de debajo.");
|
|
1711
|
+
for (const t of lista2) {
|
|
1712
|
+
const p = t.function.parameters.properties;
|
|
1713
|
+
if (p.capa === CAPA) p.capa = str("Vac\xEDo = en el slide. Para ponerlo DENTRO de una capa: su `ref`, su id (el que ense\xF1a `ver_slide`) o su nombre exacto. La capa tiene que existir en el slide donde est\xE1s trabajando (cr\xE9ala con `nueva_capa`; para una de otro slide, ve antes con `ir_a_slide`): si no, es error.");
|
|
1714
|
+
}
|
|
1706
1715
|
for (const t of lista2) {
|
|
1707
1716
|
const p = t.function.parameters.properties;
|
|
1708
1717
|
if (!t.function.name.startsWith("poner_") || !p.x || t.function.name === "poner_texto_plantilla") continue;
|
|
@@ -2058,6 +2067,78 @@ var ESQUEMAS_BOOSTERS = {
|
|
|
2058
2067
|
]
|
|
2059
2068
|
}
|
|
2060
2069
|
},
|
|
2070
|
+
magicshow: {
|
|
2071
|
+
donde: "wcfg.ext",
|
|
2072
|
+
cfgSchema: {
|
|
2073
|
+
campos: {
|
|
2074
|
+
title: texto("T\xEDtulo en el tel\xF3n: \xABWhat is missing?\xBB.", { max: 40 }),
|
|
2075
|
+
items: lista("Las cartas del show. Cada una lleva imagen, palabra o las dos.", objeto("Una carta.", {
|
|
2076
|
+
id: texto("Identificador propio de la carta. Si lo omites, lo pone el editor."),
|
|
2077
|
+
src: imagen("La imagen de la carta."),
|
|
2078
|
+
text: texto("La palabra de la carta.", { max: 30 }),
|
|
2079
|
+
anim: bool2("`src` es una animaci\xF3n Lottie (la carta se mueve sola) y no una imagen.")
|
|
2080
|
+
}), { min: 2, max: 12, obligatorio: true }),
|
|
2081
|
+
icon: objeto("El bot\xF3n m\xE1gico que se toca en clase. Sin \xE9l sale una varita.", {
|
|
2082
|
+
kind: opcion("Figura (imagen) o animaci\xF3n (Lottie).", ["image", "lottie"], { obligatorio: true }),
|
|
2083
|
+
src: texto("URL de la figura o del Lottie.", { obligatorio: true })
|
|
2084
|
+
}),
|
|
2085
|
+
names: bool2("Mostrar la palabra debajo de la imagen."),
|
|
2086
|
+
shuffle: bool2("Barajar las cartas en cada truco."),
|
|
2087
|
+
sound: bool2("Sonido del show.")
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
2090
|
+
ejemplo: {
|
|
2091
|
+
title: "What is missing?",
|
|
2092
|
+
names: true,
|
|
2093
|
+
shuffle: true,
|
|
2094
|
+
sound: true,
|
|
2095
|
+
items: [{ text: "apple" }, { text: "banana" }, { text: "orange" }, { text: "grapes" }, { text: "pear" }]
|
|
2096
|
+
}
|
|
2097
|
+
},
|
|
2098
|
+
pizarra: {
|
|
2099
|
+
donde: "wcfg.ext",
|
|
2100
|
+
cfgSchema: {
|
|
2101
|
+
campos: {
|
|
2102
|
+
fondo: opcion(
|
|
2103
|
+
"El fondo de la pizarra. \xABvidrio\xBB = cristal esmerilado sobre el slide; \xABtransparente\xBB = se escribe directo sobre el slide; \xABnegro\xBB y \xABverde\xBB = pizarr\xF3n de tiza.",
|
|
2104
|
+
["blanco", "rayas", "cuadricula", "puntos", "vidrio", "transparente", "negro", "verde"]
|
|
2105
|
+
),
|
|
2106
|
+
doc: objeto("Lo que la clase ve escrito al llegar. En el \u25B6 Player se escribe ENCIMA de una copia. Coordenadas en unidades de pizarra: 1600 \xD7 900 (la pizarra cubre el slide entero).", {
|
|
2107
|
+
trazos: lista("Trazos a mano.", objeto("Un trazo.", {
|
|
2108
|
+
id: texto("Identificador propio.", { obligatorio: true }),
|
|
2109
|
+
tipo: opcion("Pluma o resaltador (semitransparente).", ["pluma", "marcador"], { obligatorio: true }),
|
|
2110
|
+
color: color("Color de la tinta."),
|
|
2111
|
+
ancho: num2("Grosor en unidades de pizarra.", { min: 1, max: 120, entero: false, obligatorio: true }),
|
|
2112
|
+
p: lista("Los puntos, planos: x0, y0, x1, y1\u2026", num2("Una coordenada.", { entero: false }), { obligatorio: true })
|
|
2113
|
+
})),
|
|
2114
|
+
textos: lista("Textos tecleados.", objeto("Un texto.", {
|
|
2115
|
+
id: texto("Identificador propio.", { obligatorio: true }),
|
|
2116
|
+
x: num2("Borde izquierdo.", { entero: false, obligatorio: true }),
|
|
2117
|
+
y: num2("Borde de arriba.", { entero: false, obligatorio: true }),
|
|
2118
|
+
texto: texto("Lo escrito. Admite saltos de l\xEDnea.", { obligatorio: true }),
|
|
2119
|
+
color: color("Color del texto."),
|
|
2120
|
+
tam: num2("Tama\xF1o de la letra en unidades de pizarra (62 = medio).", { min: 10, max: 320, entero: false, obligatorio: true })
|
|
2121
|
+
})),
|
|
2122
|
+
imagenes: lista("Im\xE1genes pegadas.", objeto("Una imagen.", {
|
|
2123
|
+
id: texto("Identificador propio.", { obligatorio: true }),
|
|
2124
|
+
src: imagen("La imagen.", { obligatorio: true }),
|
|
2125
|
+
x: num2("Borde izquierdo.", { entero: false, obligatorio: true }),
|
|
2126
|
+
y: num2("Borde de arriba.", { entero: false, obligatorio: true }),
|
|
2127
|
+
w: num2("Ancho.", { entero: false, obligatorio: true }),
|
|
2128
|
+
h: num2("Alto.", { entero: false, obligatorio: true })
|
|
2129
|
+
}))
|
|
2130
|
+
})
|
|
2131
|
+
}
|
|
2132
|
+
},
|
|
2133
|
+
ejemplo: {
|
|
2134
|
+
fondo: "rayas",
|
|
2135
|
+
doc: {
|
|
2136
|
+
trazos: [{ id: "s1", tipo: "marcador", color: "#f59e0b", ancho: 38, p: [110, 200, 700, 200] }],
|
|
2137
|
+
textos: [{ id: "t1", x: 110, y: 120, texto: "Today: the past simple", color: "#111827", tam: 62 }],
|
|
2138
|
+
imagenes: []
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2061
2142
|
fracplot: {
|
|
2062
2143
|
donde: "wcfg.ext",
|
|
2063
2144
|
cfgSchema: {
|
|
@@ -2890,8 +2971,27 @@ function listaDestino(b, capa) {
|
|
|
2890
2971
|
}
|
|
2891
2972
|
function capaPorNombre(b, ref) {
|
|
2892
2973
|
const s = slideDe(b);
|
|
2974
|
+
return capaExacta(b, s, ref) ?? (b.estricto ? void 0 : s.layers.find((l) => l.name.toLowerCase().includes(ref.toLowerCase())));
|
|
2975
|
+
}
|
|
2976
|
+
function capaExacta(b, s, ref) {
|
|
2977
|
+
const q = ref.trim().toLowerCase();
|
|
2893
2978
|
const id = b.aliasCapa.get(ref);
|
|
2894
|
-
return s.layers.find((l) => l.id === id) ?? s.layers.find((l) => l.
|
|
2979
|
+
return s.layers.find((l) => l.id === id) ?? s.layers.find((l) => l.id === ref) ?? s.layers.find((l) => l.name.trim().toLowerCase() === q);
|
|
2980
|
+
}
|
|
2981
|
+
function capaQueFalta(b, nombre, args) {
|
|
2982
|
+
const ref = txt(args.capa).trim();
|
|
2983
|
+
if (!ref || !nombre.startsWith("poner_") || nombre === "poner_fondo") return null;
|
|
2984
|
+
const s = slideDe(b);
|
|
2985
|
+
if (capaExacta(b, s, ref)) return null;
|
|
2986
|
+
const q = ref.toLowerCase();
|
|
2987
|
+
for (const [i, sc] of b.doc.entries()) for (const [j, sl] of sc.slides.entries()) {
|
|
2988
|
+
if (sl === s) continue;
|
|
2989
|
+
const l = sl.layers.find((x) => x.id === ref || x.name.trim().toLowerCase() === q);
|
|
2990
|
+
if (l) {
|
|
2991
|
+
return `La capa \xAB${l.name}\xBB es del slide ${j + 1} de la escena ${i + 1}, y est\xE1s trabajando en el slide ${b.slide + 1} de la escena ${b.escena + 1}. Ve antes con \`ir_a_slide\` (escena ${i + 1}, slide ${j + 1}). No puse nada.`;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return `No hay ninguna capa "${ref}" en este slide (escena ${b.escena + 1}, slide ${b.slide + 1}), y no la creo: un nombre mal escrito esconder\xEDa lo que pones en una capa que nadie abre. ${s.layers.length ? `Las que hay: ${s.layers.map((l) => `\xAB${l.name}\xBB [${l.id}]`).join(", ")}.` : "Este slide no tiene capas."} Cr\xE9ala antes con \`nueva_capa\`. No puse nada.`;
|
|
2895
2995
|
}
|
|
2896
2996
|
function slideDelEl(b, el) {
|
|
2897
2997
|
for (const sc of b.doc) for (const sl of sc.slides) {
|
|
@@ -3018,10 +3118,9 @@ function armarAccion(b, a, el) {
|
|
|
3018
3118
|
const ref = txt(a.capa).trim();
|
|
3019
3119
|
if (b.estricto) {
|
|
3020
3120
|
const sl = slideDelEl(b, el) ?? s;
|
|
3021
|
-
const
|
|
3022
|
-
const capa2 = sl.layers.find((l) => l.id === id) ?? sl.layers.find((l) => l.name.trim().toLowerCase() === ref.toLowerCase());
|
|
3121
|
+
const capa2 = capaExacta(b, sl, ref);
|
|
3023
3122
|
if (!capa2) {
|
|
3024
|
-
return { accion: null, fallo: `${ref ? `No hay ninguna capa "${ref}"` : "Falta `capa`"} en el slide de ese objeto, y no la creo: un nombre mal escrito dejar\xEDa un bot\xF3n que abre una capa en blanco. ${sl.layers.length ? `Las que hay: ${sl.layers.map((l) => `\xAB${l.name}\xBB`).join(", ")}.` : "Ese slide no tiene capas."} Cr\xE9ala antes con \`nueva_capa
|
|
3123
|
+
return { accion: null, fallo: `${ref ? `No hay ninguna capa "${ref}"` : "Falta `capa`"} en el slide de ese objeto, y no la creo: un nombre mal escrito dejar\xEDa un bot\xF3n que abre una capa en blanco. ${sl.layers.length ? `Las que hay: ${sl.layers.map((l) => `\xAB${l.name}\xBB`).join(", ")}.` : "Ese slide no tiene capas."} Cr\xE9ala antes con \`nueva_capa\`.` };
|
|
3025
3124
|
}
|
|
3026
3125
|
return { accion: { type: "showLayer", layerId: capa2.id } };
|
|
3027
3126
|
}
|
|
@@ -3108,6 +3207,15 @@ function armarAccion(b, a, el) {
|
|
|
3108
3207
|
if (audio2.kind !== "audio") return { accion: null, fallo: `"${d}" no es un audio.` };
|
|
3109
3208
|
return { accion: { type: "playSound", targetId: audio2.id } };
|
|
3110
3209
|
}
|
|
3210
|
+
case "pausarAudio":
|
|
3211
|
+
case "detenerAudio": {
|
|
3212
|
+
const d = txt(a.destino).trim();
|
|
3213
|
+
const audio2 = d ? buscarEl(b, d) : el.kind === "audio" ? el : null;
|
|
3214
|
+
if (d && !audio2) return { accion: null, fallo: `No encuentro el audio "${d}".` };
|
|
3215
|
+
if (audio2 && audio2.kind !== "audio") return { accion: null, fallo: `"${d}" no es un audio.` };
|
|
3216
|
+
const type = txt(a.accion) === "pausarAudio" ? "pauseSound" : "stopSound";
|
|
3217
|
+
return { accion: { type, targetId: audio2?.id ?? "" } };
|
|
3218
|
+
}
|
|
3111
3219
|
case "ocultar": {
|
|
3112
3220
|
const d = txt(a.destino).trim();
|
|
3113
3221
|
const otro = d ? buscarEl(b, d) : null;
|
|
@@ -3120,6 +3228,10 @@ function armarAccion(b, a, el) {
|
|
|
3120
3228
|
}
|
|
3121
3229
|
function ejecutar(b, nombre, args, fab) {
|
|
3122
3230
|
const antes = b.creados.length;
|
|
3231
|
+
if (b.estricto) {
|
|
3232
|
+
const falta = capaQueFalta(b, nombre, args);
|
|
3233
|
+
if (falta) return no(b, falta);
|
|
3234
|
+
}
|
|
3123
3235
|
const dijo = ejecutarUna(b, nombre, args, fab);
|
|
3124
3236
|
return b.estricto && !b.fallo ? dijo + dejarFuera(b, nombre, args, antes) : dijo;
|
|
3125
3237
|
}
|
|
@@ -3799,7 +3911,7 @@ function describirTrigger(t, b) {
|
|
|
3799
3911
|
const cuando = t.event.type === "click" ? "al tocarlo" : t.event.type === "hover" ? "al pasarle por encima" : t.event.type === "slideStart" ? "al entrar al slide" : t.event.type === "layerShown" ? "al abrirse su capa" : t.event.type === "time" ? `a los ${t.event.seconds} s` : t.event.type === "clickStep" ? `en el clic n\xBA ${t.event.step}` : t.event.type === "motionEnd" ? "al terminar su trayectoria" : "al dispararse";
|
|
3800
3912
|
const a = t.action;
|
|
3801
3913
|
const nombreCapa = (id) => buscarCapa(b, id)?.name ?? "una capa";
|
|
3802
|
-
const qu\u00E9 = a.type === "showLayer" ? `abre \xAB${nombreCapa(a.layerId)}\xBB` : a.type === "goScene" ? `salta a \xAB${b.doc.find((s) => s.id === a.sceneId)?.name ?? "otra escena"}\xBB` : a.type === "goSlide" ? "salta a otro slide" : a.type === "closeLayer" ? "cierra la capa" : a.type === "speak" ? `lee \xAB${a.text}\xBB en voz alta` : a.type === "openUrl" ? "abre un enlace" : a.type === "hideEl" ? "desaparece con humo" : a.type === "playMotion" ? "recorre su trayectoria" : a.type === "playSound" ? "suena el audio" : a.type === "resetSequence" ? "reinicia el slide" : "hace lo suyo";
|
|
3914
|
+
const qu\u00E9 = a.type === "showLayer" ? `abre \xAB${nombreCapa(a.layerId)}\xBB` : a.type === "goScene" ? `salta a \xAB${b.doc.find((s) => s.id === a.sceneId)?.name ?? "otra escena"}\xBB` : a.type === "goSlide" ? "salta a otro slide" : a.type === "closeLayer" ? "cierra la capa" : a.type === "speak" ? `lee \xAB${a.text}\xBB en voz alta` : a.type === "openUrl" ? "abre un enlace" : a.type === "hideEl" ? "desaparece con humo" : a.type === "playMotion" ? "recorre su trayectoria" : a.type === "playSound" ? "suena el audio" : a.type === "pauseSound" ? "pausa el audio" : a.type === "stopSound" ? "detiene el audio" : a.type === "resetSequence" ? "reinicia el slide" : "hace lo suyo";
|
|
3803
3915
|
return `${cuando}, ${qu\u00E9}`;
|
|
3804
3916
|
}
|
|
3805
3917
|
|
|
@@ -3840,13 +3952,22 @@ var WORDSTORM_SAMPLE = [
|
|
|
3840
3952
|
var seq = 0;
|
|
3841
3953
|
var cycleUid = () => `cs-${Date.now().toString(36)}-${seq++}`;
|
|
3842
3954
|
|
|
3955
|
+
// ../src/canvas/widgets/MagicShowWidget.datos.ts
|
|
3956
|
+
var seq2 = 0;
|
|
3957
|
+
var magicShowUid = () => `ms-${Date.now().toString(36)}-${seq2++}`;
|
|
3958
|
+
var MAGICSHOW_DEMO = ["apple", "cat", "sun", "book", "star", "fish"];
|
|
3959
|
+
|
|
3960
|
+
// ../src/canvas/widgets/PizarraWidget.datos.ts
|
|
3961
|
+
var pzVacio = () => ({ trazos: [], textos: [], imagenes: [] });
|
|
3962
|
+
var PIZARRA_DEFAULTS = { fondo: "blanco", doc: pzVacio() };
|
|
3963
|
+
|
|
3843
3964
|
// ../src/canvas/widgets/plot2d.ts
|
|
3844
3965
|
var CURVE_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#d97706", "#7c3aed", "#db2777", "#0891b2", "#65a30d"];
|
|
3845
3966
|
|
|
3846
3967
|
// ../src/canvas/widgets/EquationPlotterWidget.datos.ts
|
|
3847
3968
|
var DEFAULT_VIEW = { cx: 0, cy: 0, scale: 46 };
|
|
3848
|
-
var
|
|
3849
|
-
var eid = () => `eq-${Date.now().toString(36)}-${
|
|
3969
|
+
var seq3 = 0;
|
|
3970
|
+
var eid = () => `eq-${Date.now().toString(36)}-${seq3++}`;
|
|
3850
3971
|
function defaultPlotterCfg() {
|
|
3851
3972
|
return {
|
|
3852
3973
|
eqs: [{ id: eid(), src: "y = x^2", color: CURVE_COLORS[0], visible: true }],
|
|
@@ -4126,17 +4247,17 @@ var WIDGETS = [
|
|
|
4126
4247
|
// de la interfaz). El precio del recorte malo es peor que el de la GPU: por eso NO hay carril
|
|
4127
4248
|
// barato de reserva — ver la cabecera de `quitarFondo.ts`.
|
|
4128
4249
|
{ type: "quitafondo", emoji: "\u{1F98E}", label: "Quita Fondos", blurb: "Arr\xE1stralo sobre una imagen y el camale\xF3n se come el fondo. Recorta con el modelo grande: separa mechones de pelo, hojas y patas.", premium: true, ready: true },
|
|
4129
|
-
{ type: "transformar", emoji: "\u{1F500}", label: "Transformador", blurb: "Arr\xE1stralo sobre un objeto y elige en qu\xE9 se convierte. En el \u25B6 Player cada clic lo transforma en la siguiente imagen \u2014de la oruga a la mariposa\u2014 y la \xFAltima lo devuelve a como estaba.", premium:
|
|
4130
|
-
{ type: "magichat", emoji: "\u{1F3A9}", label: "Sombrero M\xE1gico", blurb: "Arr\xE1stralo sobre una imagen (o PNG) para volverla una silueta negra. En el \u25B6 Player, al hacer clic se revela a color con una lluvia de estrellas m\xE1gicas.", premium:
|
|
4250
|
+
{ type: "transformar", emoji: "\u{1F500}", label: "Transformador", blurb: "Arr\xE1stralo sobre un objeto y elige en qu\xE9 se convierte. En el \u25B6 Player cada clic lo transforma en la siguiente imagen \u2014de la oruga a la mariposa\u2014 y la \xFAltima lo devuelve a como estaba.", premium: true, ready: true },
|
|
4251
|
+
{ type: "magichat", emoji: "\u{1F3A9}", label: "Sombrero M\xE1gico", blurb: "Arr\xE1stralo sobre una imagen (o PNG) para volverla una silueta negra. En el \u25B6 Player, al hacer clic se revela a color con una lluvia de estrellas m\xE1gicas.", premium: true, ready: true },
|
|
4131
4252
|
// Tómbola: bombo de bingo que gira y suelta una bola (número/palabra/imagen), sin repetir.
|
|
4132
|
-
{ type: "tombola", emoji: "\u{1F3B0}", label: "T\xF3mbola", blurb: "Bombo de bingo: gira y suelta una bola al azar con un n\xFAmero, palabra o imagen. Sortea sin repetir hasta vaciarse. Ideal para rifas y elegir al azar.", premium:
|
|
4253
|
+
{ type: "tombola", emoji: "\u{1F3B0}", label: "T\xF3mbola", blurb: "Bombo de bingo: gira y suelta una bola al azar con un n\xFAmero, palabra o imagen. Sortea sin repetir hasta vaciarse. Ideal para rifas y elegir al azar.", premium: true, ready: true },
|
|
4133
4254
|
// Semáforo de comportamiento: 3 luces que el maestro toca en el Player para dar feedback a la clase.
|
|
4134
4255
|
{ type: "trafficlight", emoji: "\u{1F6A6}", label: "Sem\xE1foro", blurb: "Sem\xE1foro de comportamiento: en el Player, toca una luz (verde/\xE1mbar/rojo) para dar feedback a la clase. El rojo dispara una alerta sonora y visual.", premium: false, ready: true },
|
|
4135
4256
|
// Graficador de ecuaciones (app-native, rama de MATEMÁTICA): orbe → estudio 2D/3D.
|
|
4136
4257
|
// three.js entra por lazy() solo si el maestro pulsa 3D.
|
|
4137
4258
|
// Label It (app-native, rama de CIENCIAS): la lámina del maestro se vuelve actividad de etiquetar.
|
|
4138
4259
|
// Sirve a biología (célula, flor, corazón), pero también a geografía, anatomía o vocabulario.
|
|
4139
|
-
{ type: "labelit", emoji: "\u{1F3F7}\uFE0F", label: "Label It \u2014 etiquetar", blurb: "Sube cualquier l\xE1mina (c\xE9lula, flor, coraz\xF3n, mapa, una cara\u2026), toca la imagen para clavar los puntos y escribe el nombre de cada parte. En el \u25B6 Player se juega de tres formas: revelar al tocar, arrastrar las palabras a su punto, o escribirlas.", premium:
|
|
4260
|
+
{ type: "labelit", emoji: "\u{1F3F7}\uFE0F", label: "Label It \u2014 etiquetar", blurb: "Sube cualquier l\xE1mina (c\xE9lula, flor, coraz\xF3n, mapa, una cara\u2026), toca la imagen para clavar los puntos y escribe el nombre de cada parte. En el \u25B6 Player se juega de tres formas: revelar al tocar, arrastrar las palabras a su punto, o escribirlas.", premium: true, ready: true },
|
|
4140
4261
|
// Ciclo (app-native, rama de CIENCIAS): una rueda de etapas reemplaza cinco slides sueltas.
|
|
4141
4262
|
// La rueda NO gira (gira el FOCO), así las imágenes quedan siempre derechas en la proyección.
|
|
4142
4263
|
// 🍕 «Fracciones manipulables» (regletas que se cortan) SE RETIRÓ del catálogo el 2026-08-12:
|
|
@@ -4144,21 +4265,26 @@ var WIDGETS = [
|
|
|
4144
4265
|
// armar sus láminas. El booster NO se borra —solo su tarjeta—: los slides que ya lo tengan
|
|
4145
4266
|
// puesto tienen que seguir abriendo y funcionando (su render, su editor y su config siguen
|
|
4146
4267
|
// cableados más abajo). Para devolverlo al catálogo, basta con volver a poner su entrada aquí.
|
|
4147
|
-
{ type: "cycle", emoji: "\u{1F504}", label: "Ciclo \u2014 rueda de etapas", blurb: "Rueda con las etapas de un proceso (metamorfosis, mitosis, ciclo del agua, fotos\xEDntesis\u2026), cada una con su imagen y su texto. En el \u25B6 Player, cada clic mueve el foco a la siguiente y el centro cuenta qu\xE9 pasa ah\xED. Puede cerrar en c\xEDrculo o ser una cadena, y los nombres pueden salir ocultos para que la clase adivine.", premium:
|
|
4268
|
+
{ type: "cycle", emoji: "\u{1F504}", label: "Ciclo \u2014 rueda de etapas", blurb: "Rueda con las etapas de un proceso (metamorfosis, mitosis, ciclo del agua, fotos\xEDntesis\u2026), cada una con su imagen y su texto. En el \u25B6 Player, cada clic mueve el foco a la siguiente y el centro cuenta qu\xE9 pasa ah\xED. Puede cerrar en c\xEDrculo o ser una cadena, y los nombres pueden salir ocultos para que la clase adivine.", premium: true, ready: true },
|
|
4269
|
+
// Show de Magia: «¿qué desapareció?» con teatro, sombrero y un botón mágico que elige el maestro.
|
|
4270
|
+
{ type: "magicshow", emoji: "\u{1F3A9}", label: "Show de Magia \u2014 \xBFqu\xE9 desapareci\xF3?", blurb: "Carga varias im\xE1genes (con su palabra o sin ella) y ponlas en un escenario de teatro. En el \u25B6 Player, cada clic en el bot\xF3n m\xE1gico mete las cartas al sombrero, las baraja y una desaparece; el siguiente clic la hace reaparecer. Nunca repite hasta que salgan todas. El bot\xF3n puede ser la varita o cualquier figura o animaci\xF3n de la biblioteca.", premium: true, ready: true },
|
|
4148
4271
|
// Graficador de fracciones (app-native, rama de MATEMÁTICA): HERRAMIENTA de autoría. No es una
|
|
4149
4272
|
// actividad para la clase: es la moneda con la que el maestro DIBUJA las fracciones de su
|
|
4150
4273
|
// lámina, una tras otra, sin salir a buscar imágenes fuera.
|
|
4151
|
-
{ type: "fracplot", emoji: "\u{1F355}", label: "Graficador de fracciones", blurb: "Escribe una fracci\xF3n en sus dos casillas \u2014abajo en cu\xE1ntos pedazos se corta, arriba cu\xE1ntos se pintan\u2014 y la dibuja al instante. Cada \xABInsertar\xBB la estampa en el slide como una figura que se mueve y se estira como cualquier imagen, y el panel se queda abierto para pedir la siguiente: 3/4, luego 2/3, luego 5/8\u2026 Pizza o barra, con la fracci\xF3n escrita debajo. Una impropia como 4/3 sale como dos pizzas.", premium:
|
|
4274
|
+
{ type: "fracplot", emoji: "\u{1F355}", label: "Graficador de fracciones", blurb: "Escribe una fracci\xF3n en sus dos casillas \u2014abajo en cu\xE1ntos pedazos se corta, arriba cu\xE1ntos se pintan\u2014 y la dibuja al instante. Cada \xABInsertar\xBB la estampa en el slide como una figura que se mueve y se estira como cualquier imagen, y el panel se queda abierto para pedir la siguiente: 3/4, luego 2/3, luego 5/8\u2026 Pizza o barra, con la fracci\xF3n escrita debajo. Una impropia como 4/3 sale como dos pizzas.", premium: true, ready: true },
|
|
4152
4275
|
// QR: el puente entre el slide proyectado y el celular del alumno. Estático a
|
|
4153
4276
|
// propósito (el enlace va grabado en el dibujo): cero infraestructura, cero costo
|
|
4154
4277
|
// mensual y sigue funcionando dentro del .zip exportado a LearnPress.
|
|
4155
4278
|
{ type: "qr", emoji: "\u{1F533}", label: "QR \u2014 el que se come el enlace", blurb: "Pon un enlace y sale un c\xF3digo QR que la clase escanea con el celular: la tarea, el formulario, un video, la sala de Booster Live. Al pulsar \xABGenerar\xBB, el c\xF3digo se COME el enlace: abre la boca y se traga a bocados unos c\xF3digos QR peque\xF1os; al cerrarla, los m\xF3dulos que faltaban caen en su sitio. Color, forma de los puntos y r\xF3tulo a tu gusto.", premium: false, ready: true },
|
|
4279
|
+
// Pizarra (app-native): escribir a mano, teclear y pegar imágenes DENTRO del slide. Para el
|
|
4280
|
+
// pizarrón táctil del aula y para la clase por Zoom. Lo de la clase no toca lo guardado.
|
|
4281
|
+
{ type: "pizarra", emoji: "\u{1F58D}\uFE0F", label: "Pizarra blanca", blurb: "Una moneda que en el \u25B6 Player abre una pizarra encima de todo el slide: escribe a mano con el dedo, el l\xE1piz o el rat\xF3n, teclea texto y pega im\xE1genes. Pluma, resaltador, borrador, colores y deshacer; la \u2715 roja la quita. Papel blanco, de rayas, cuadros, puntos, vidrio, transparente o pizarr\xF3n de tiza. Ideal para pizarras t\xE1ctiles y clases por Zoom: lo que se escribe en clase no toca tu planificaci\xF3n.", premium: false, ready: true },
|
|
4156
4282
|
{ type: "plotter", emoji: "\u{1F4C8}", label: "Graficador 2D \xB7 3D", blurb: "Orbe que abre un graficador de ecuaciones. En 2D: funciones, polares, param\xE9tricas, c\xEDrculos y elipses, inecuaciones, y los CORTES entre curvas marcados con sus coordenadas. En 3D: superficies z = f(x,y) que se giran con el dedo. Cada letra suelta (a, b, c) se vuelve un deslizador.", premium: true, ready: true }
|
|
4157
4283
|
];
|
|
4158
|
-
var APP_NATIVE_BOOSTERS = ["instructions", "magicwand", "magichat", "quitafondo", "transformar", "tombola", "trafficlight", "plotter", "labelit", "cycle", "fractions", "fracplot", "qr"];
|
|
4284
|
+
var APP_NATIVE_BOOSTERS = ["instructions", "magicwand", "magichat", "quitafondo", "transformar", "tombola", "trafficlight", "plotter", "labelit", "cycle", "magicshow", "fractions", "fracplot", "qr", "pizarra"];
|
|
4159
4285
|
var ARENAS_APP = [
|
|
4160
4286
|
{ type: "classification", emoji: "\u{1F6F8}", label: "Clasificaci\xF3n \xB7 Espacio", blurb: "Arrastra cada palabra a su categor\xEDa correcta. Tema espacial con OVNIs y aliens.", premium: false, ready: true },
|
|
4161
|
-
{ type: "classification-jungle", emoji: "\u{1F435}", label: "Clasificaci\xF3n \xB7 Selva", blurb: "Arrastra cada monito a su bungal\xF3 correcto. Tema selva con caba\xF1as de bamb\xFA.", premium:
|
|
4287
|
+
{ type: "classification-jungle", emoji: "\u{1F435}", label: "Clasificaci\xF3n \xB7 Selva", blurb: "Arrastra cada monito a su bungal\xF3 correcto. Tema selva con caba\xF1as de bamb\xFA.", premium: true, ready: true },
|
|
4162
4288
|
{ type: "gameshow", emoji: "\u{1F3AD}", label: "Game Show", blurb: "Concurso de 2 estudiantes: tel\xF3n, ruleta, preguntas con tiempo y podio.", premium: true, ready: true },
|
|
4163
4289
|
{ type: "race", emoji: "\u{1F3CE}\uFE0F", label: "Carrera de Karts", blurb: "Carrera estilo F\xF3rmula 1: hasta 4 jugadores eligen corredor, responden preguntas y ganan nitro.", premium: true, ready: true },
|
|
4164
4290
|
{ type: "pirate", emoji: "\u{1F3F4}\u200D\u2620\uFE0F", label: "Batalla Pirata", blurb: "Concurso de preguntas para TODA la clase desde sus celulares: gana oro por contestar r\xE1pido, y el que acierta abre un cofre que puede quitarle oro a un compa\xF1ero.", premium: true, ready: true },
|
|
@@ -4167,19 +4293,20 @@ var ARENAS_APP = [
|
|
|
4167
4293
|
{ type: "memory", emoji: "\u{1F0CF}", label: "Memory Game", blurb: "Encuentra las parejas ocultas. Cada carta acepta imagen, audio y texto: imagen\u2194imagen con audio, imagen\u2194palabra o palabra\u2194definici\xF3n. Individual o por equipos.", premium: false, ready: true },
|
|
4168
4294
|
// Word Storm nació en we-boosters y se trajo app-native (ver APP_NATIVE_ARENAS): su ficha
|
|
4169
4295
|
// ya no viene del puente, así que vive aquí como cualquier arena de la app.
|
|
4170
|
-
{ type: "booster-escape", emoji: "\u{1F9ED}", label: "Booster Escape", blurb: "Laberinto a contrarreloj con dos modos: Individual (corre por los pasillos, esquiva a los enemigos y llega a la respuesta correcta; 3 vidas) y Versus 1v1. Un toque y el explorador corre solo hasta topar. Edita las preguntas (con imagen/audio).", premium:
|
|
4171
|
-
{ type: "las-vegas", emoji: "\u{1F3B0}", label: "The Fabulous Las Vegas", blurb: "Tragamonedas para construir frases: jala la palanca, los rodillos frenan uno a uno y sale una combinaci\xF3n \u2014SHE \xB7 WRITTEN \xB7 A LETTER\u2014 que la clase dice en voz alta. De 1 a 4 rodillos, y cada casilla puede ser una palabra o un dibujo.", premium:
|
|
4296
|
+
{ type: "booster-escape", emoji: "\u{1F9ED}", label: "Booster Escape", blurb: "Laberinto a contrarreloj con dos modos: Individual (corre por los pasillos, esquiva a los enemigos y llega a la respuesta correcta; 3 vidas) y Versus 1v1. Un toque y el explorador corre solo hasta topar. Edita las preguntas (con imagen/audio).", premium: true, ready: true },
|
|
4297
|
+
{ type: "las-vegas", emoji: "\u{1F3B0}", label: "The Fabulous Las Vegas", blurb: "Tragamonedas para construir frases: jala la palanca, los rodillos frenan uno a uno y sale una combinaci\xF3n \u2014SHE \xB7 WRITTEN \xB7 A LETTER\u2014 que la clase dice en voz alta. De 1 a 4 rodillos, y cada casilla puede ser una palabra o un dibujo.", premium: true, ready: true },
|
|
4172
4298
|
// Sentence Line (app-native): la hermana de Word Factory — mismo motor, pero la pieza es una
|
|
4173
4299
|
// PALABRA y lo que se arma es una oración.
|
|
4174
|
-
{ type: "sentenceline", emoji: "\u{1F309}", label: "Sentence Bridge", blurb: "El puente de tablones: cada palabra es un tabl\xF3n y hay que ponerlas en orden para armar el puente. El explorador salta al tabl\xF3n que acabas de poner, y si la oraci\xF3n no es, los tablones equivocados se caen al r\xEDo. Cuando la frase queda bien, cruza al otro lado. Se parte sola por los espacios, o marcas trozos con barras \u2014\xABThe black cat / is sleeping / on the sofa\xBB\u2014 para ense\xF1ar la estructura.", premium:
|
|
4300
|
+
{ type: "sentenceline", emoji: "\u{1F309}", label: "Sentence Bridge", blurb: "El puente de tablones: cada palabra es un tabl\xF3n y hay que ponerlas en orden para armar el puente. El explorador salta al tabl\xF3n que acabas de poner, y si la oraci\xF3n no es, los tablones equivocados se caen al r\xEDo. Cuando la frase queda bien, cruza al otro lado. Se parte sola por los espacios, o marcas trozos con barras \u2014\xABThe black cat / is sleeping / on the sofa\xBB\u2014 para ense\xF1ar la estructura.", premium: true, ready: true },
|
|
4175
4301
|
// Word Factory (app-native): deletrear ordenando las letras que llegan por la cinta.
|
|
4176
|
-
{ type: "wordfactory", emoji: "\u{1F3ED}", label: "Word Factory", blurb: "Una f\xE1brica de palabras: entra un dibujo y sus letras llegan revueltas por la cinta transportadora. El alumno las mete en la m\xE1quina en el orden correcto y, cuando la palabra sale bien, la m\xE1quina fabrica el objeto y lo sella. Sirve para cualquier vocabulario: sube tu dibujo y escribe su palabra.", premium:
|
|
4302
|
+
{ type: "wordfactory", emoji: "\u{1F3ED}", label: "Word Factory", blurb: "Una f\xE1brica de palabras: entra un dibujo y sus letras llegan revueltas por la cinta transportadora. El alumno las mete en la m\xE1quina en el orden correcto y, cuando la palabra sale bien, la m\xE1quina fabrica el objeto y lo sella. Sirve para cualquier vocabulario: sube tu dibujo y escribe su palabra.", premium: true, ready: true },
|
|
4177
4303
|
// Mazo de Cartas (app-native): reemplaza la actividad de cartas de Wordwall. El maestro
|
|
4178
4304
|
// toca el mazo y sale una carta al azar; la descarta o la devuelve al mazo.
|
|
4179
4305
|
{ type: "deck", emoji: "\u{1F3B4}", label: "Mazo de Cartas", blurb: "Un mazo boca abajo: toca y sale una carta al azar, volte\xE1ndose en el aire. Cada carta lleva una palabra, una pregunta o un dibujo. La descartas o la devuelves al mazo, y con un bot\xF3n se mezcla otra vez. Para conversaci\xF3n, vocabulario, verbos o tarjetas de rol.", premium: false, ready: true },
|
|
4180
|
-
{ type: "word-storm", emoji: "\u2328\uFE0F", label: "Word Storm", blurb: "Lluvia de palabras: escr\xEDbelas con el teclado para destruirlas antes de que toquen la l\xEDnea de peligro. La dificultad sube solita. Edita la lista de palabras.", premium:
|
|
4306
|
+
{ type: "word-storm", emoji: "\u2328\uFE0F", label: "Word Storm", blurb: "Lluvia de palabras: escr\xEDbelas con el teclado para destruirlas antes de que toquen la l\xEDnea de peligro. La dificultad sube solita. Edita la lista de palabras.", premium: true, ready: true }
|
|
4181
4307
|
];
|
|
4182
|
-
var soloFicha = (e) => ({ type: e.type, emoji: e.emoji, label: e.label, blurb: e.blurb, premium: e.premium, ready: e.ready });
|
|
4308
|
+
var soloFicha = (e) => ({ type: e.type, emoji: e.emoji, label: e.label, blurb: e.blurb, premium: e.premium || PREMIUM_EXTERNAS.has(e.type), ready: e.ready });
|
|
4309
|
+
var PREMIUM_EXTERNAS = /* @__PURE__ */ new Set(["jenga", "we-spelling-jenga", "duck-shooting"]);
|
|
4183
4310
|
var ALL_WIDGETS = [
|
|
4184
4311
|
...WIDGETS,
|
|
4185
4312
|
...FICHAS_BOOSTERS.filter((e) => !APP_NATIVE_BOOSTERS.includes(e.type)).map(soloFicha)
|
|
@@ -4204,8 +4331,13 @@ function construirWidget(type, manos) {
|
|
|
4204
4331
|
guess: false,
|
|
4205
4332
|
sound: true,
|
|
4206
4333
|
stages: Array.from({ length: 4 }, () => ({ id: cycleUid(), text: "" }))
|
|
4207
|
-
} } : type === "
|
|
4208
|
-
|
|
4334
|
+
} } : type === "magicshow" ? { ext: {
|
|
4335
|
+
names: true,
|
|
4336
|
+
shuffle: true,
|
|
4337
|
+
sound: true,
|
|
4338
|
+
items: MAGICSHOW_DEMO.map((text) => ({ id: magicShowUid(), text }))
|
|
4339
|
+
} } : type === "fractions" ? { ext: { mode: "add", title: "", a: "1/2", b: "1/3", denoms: FRAC_DENOMS, labels: true, sound: true } } : type === "qr" ? { ext: { ...QR_DEFAULTS, gen: 0 } } : type === "pizarra" ? { ext: { ...PIZARRA_DEFAULTS, doc: pzVacio() } } : type === "wheel" ? { names: namesDefault } : type === "timer" ? { seconds: 60 } : type === "score" ? { teams: ["Equipo A", "Equipo B"] } : type === "groups" ? { names: namesDefault, teamCount: 2 } : type === "mystery" ? { items: ["apple", "happy", "run", "book", "blue", "friend", "school", "water"] } : { dice: 1 };
|
|
4340
|
+
const sz = ext ? ext.defaultSize : type === "trafficlight" ? { w: 16, h: 42 } : type === "labelit" ? { w: 52, h: 68 } : type === "cycle" ? { w: 66, h: 64 } : type === "magicshow" ? { w: 62, h: 62 } : type === "fractions" ? { w: 74, h: 50 } : type === "qr" ? { w: 16, h: 33 } : { w: 7, h: 12.4 };
|
|
4209
4341
|
const pos = sz.h > 55 ? { x: (100 - sz.w) / 2, y: (100 - sz.h) / 2 } : p;
|
|
4210
4342
|
return { ok: true, el: { id: manos.uid("wdg"), kind: "widget", widget: type, x: pos.x, y: pos.y, w: sz.w, h: sz.h, wcfg } };
|
|
4211
4343
|
}
|
package/package.json
CHANGED
package/proyectos.mjs
CHANGED
|
@@ -154,7 +154,12 @@ export async function resolverDestino(token, { equipo, carpeta }) {
|
|
|
154
154
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
155
155
|
const YO = `node ${process.argv[1]}`;
|
|
156
156
|
const [, , orden, ...resto] = process.argv;
|
|
157
|
-
const token = await tokenDeAcceso()
|
|
157
|
+
const token = await tokenDeAcceso().catch((e) => {
|
|
158
|
+
console.error(`
|
|
159
|
+
✕ ${e.message}
|
|
160
|
+
`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
158
163
|
if (!token) {
|
|
159
164
|
console.error('\n ✕ No hay sesión. Entra primero con `sesion.mjs entrar`.\n');
|
|
160
165
|
process.exit(1);
|
package/servidor.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { join } from 'node:path';
|
|
|
30
30
|
import { existsSync } from 'node:fs';
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
32
|
import { createRequire } from 'node:module';
|
|
33
|
-
import { tokenDeAcceso } from './sesion.mjs';
|
|
33
|
+
import { alRenovar, tokenDeAcceso } from './sesion.mjs';
|
|
34
34
|
import { abrir, carpetas, crear, equipos, guardar, listar, resolverDestino } from './proyectos.mjs';
|
|
35
35
|
import { subirArchivo, subirBytes } from './subidas.mjs';
|
|
36
36
|
import { rematarEncargo } from './encargos.mjs';
|
|
@@ -104,6 +104,10 @@ const fabricas = () => ({
|
|
|
104
104
|
nuevaArena: (t) => nucleo.construirArena(t, manos()),
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
// Cada refresco de la sesión queda en el registro. Lo normal es ~1 por hora: si salen muchos
|
|
108
|
+
// seguidos, algo vuelve a renovar de más (ver `tokenDeAcceso()`).
|
|
109
|
+
alRenovar(({ refrescos, expira }) => aviso(`sesión renovada (#${refrescos}), vale hasta ${expira}`));
|
|
110
|
+
|
|
107
111
|
async function token() {
|
|
108
112
|
const t = await tokenDeAcceso();
|
|
109
113
|
if (!t) throw new Error('No hay sesión en este equipo. Corre `node mcp/sesion.mjs entrar` y vuelve a arrancar el agente.');
|
|
@@ -146,6 +150,10 @@ function herramientasDelLienzo() {
|
|
|
146
150
|
}));
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
const MAX_TANDA = 300;
|
|
154
|
+
// Lo que cambia de proyecto (dejaría el resto de la lista cayendo en otro) y la propia tanda.
|
|
155
|
+
const NO_EN_TANDA = new Set(['tanda', 'abrir_proyecto', 'crear_proyecto']);
|
|
156
|
+
|
|
149
157
|
const HERRAMIENTAS_PROYECTO = [
|
|
150
158
|
{
|
|
151
159
|
name: 'listar_proyectos',
|
|
@@ -156,13 +164,34 @@ const HERRAMIENTAS_PROYECTO = [
|
|
|
156
164
|
{
|
|
157
165
|
name: 'abrir_proyecto',
|
|
158
166
|
description: 'Abre un proyecto y lo deja LISTO para trabajar. A partir de aquí, todo lo que pongas '
|
|
159
|
-
+ 'cae en él. Hay que llamarlo antes que cualquier herramienta del lienzo.'
|
|
167
|
+
+ 'cae en él. Hay que llamarlo antes que cualquier herramienta del lienzo. Sin `slide`, se trabaja '
|
|
168
|
+
+ 'en el slide 1 de la escena 1; para CORREGIR un slide ya hecho, pásale `escena` y `slide` (o '
|
|
169
|
+
+ 'usa `ir_a_slide` después): entonces devuelve una línea por slide y el detalle SOLO de ese, que '
|
|
170
|
+
+ 'cuesta mucho menos leer que el proyecto entero.',
|
|
160
171
|
inputSchema: {
|
|
161
172
|
type: 'object',
|
|
162
|
-
properties: {
|
|
173
|
+
properties: {
|
|
174
|
+
id: { type: 'string', description: 'El id que devolvió `listar_proyectos`.' },
|
|
175
|
+
escena: { type: 'string', description: 'En qué escena trabajar: su número empezando en 1, o su nombre. Sin esto, la 1.' },
|
|
176
|
+
slide: { type: 'string', description: 'En qué slide trabajar: su número empezando en 1, su nombre o su id. Sin esto, el 1.' },
|
|
177
|
+
},
|
|
163
178
|
required: ['id'],
|
|
164
179
|
},
|
|
165
180
|
},
|
|
181
|
+
{
|
|
182
|
+
name: 'ir_a_slide',
|
|
183
|
+
description: 'Cambia el slide de TRABAJO del proyecto abierto a uno que ya existe, para corregirlo '
|
|
184
|
+
+ 'sin rehacer nada. Todo lo que pongas después (y el `capa` de las herramientas de poner) es de '
|
|
185
|
+
+ 'ese slide. Devuelve lo que tiene, con los ids de sus objetos y capas.',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
escena: { type: 'string', description: 'Su número empezando en 1, o su nombre. Sin esto, la escena en la que estás.' },
|
|
190
|
+
slide: { type: 'string', description: 'Su número empezando en 1 dentro de esa escena, su nombre o su id.' },
|
|
191
|
+
},
|
|
192
|
+
required: ['slide'],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
166
195
|
{
|
|
167
196
|
name: 'crear_proyecto',
|
|
168
197
|
description: 'Crea una presentación nueva, vacía, y la deja abierta para trabajar. Devuelve su id. '
|
|
@@ -285,6 +314,34 @@ const HERRAMIENTAS_PROYECTO = [
|
|
|
285
314
|
required: [],
|
|
286
315
|
},
|
|
287
316
|
},
|
|
317
|
+
{
|
|
318
|
+
name: 'tanda',
|
|
319
|
+
description: 'Hace VARIAS órdenes en una sola llamada, en orden, sobre el proyecto abierto. Es la forma '
|
|
320
|
+
+ 'BARATA de armar un slide: en vez de 100 llamadas sueltas (y 100 vueltas de pensar), una lista. '
|
|
321
|
+
+ 'Cada orden es { herramienta, args } con los mismos nombres y argumentos que la herramienta suelta; '
|
|
322
|
+
+ 'los `ref` de una orden valen para las siguientes. Se para en la primera que falle (lo de antes ya '
|
|
323
|
+
+ 'queda puesto) y te dice cuál y por qué. No sirve para abrir o crear proyectos. Guarda al final '
|
|
324
|
+
+ 'con `guardar_proyecto` (puede ser la última orden de la lista).',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: {
|
|
328
|
+
ordenes: {
|
|
329
|
+
type: 'array',
|
|
330
|
+
description: `De 1 a ${MAX_TANDA} órdenes.`,
|
|
331
|
+
items: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
herramienta: { type: 'string', description: 'Nombre de la herramienta. Ej: "poner_texto".' },
|
|
335
|
+
args: { type: 'object', description: 'Sus argumentos, como si la llamaras sola.' },
|
|
336
|
+
},
|
|
337
|
+
required: ['herramienta'],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
seguir_si_falla: { type: 'boolean', description: 'true = si una falla, sigue con las demás. Sin esto, se para.' },
|
|
341
|
+
},
|
|
342
|
+
required: ['ordenes'],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
288
345
|
];
|
|
289
346
|
|
|
290
347
|
// ── Ejecutar una herramienta ─────────────────────────────────────────────────
|
|
@@ -336,6 +393,8 @@ function queHace(t, doc, slide) {
|
|
|
336
393
|
: a.type === 'goSlide' ? 'va a otro slide'
|
|
337
394
|
: a.type === 'closeLayer' ? 'cierra la capa'
|
|
338
395
|
: a.type === 'playSound' ? `suena ${a.targetId}`
|
|
396
|
+
: a.type === 'pauseSound' ? `pausa ${a.targetId || 'el audio'}`
|
|
397
|
+
: a.type === 'stopSound' ? `detiene ${a.targetId || 'el audio'}`
|
|
339
398
|
: a.type === 'speak' ? `lee «${corto(a.text, 24)}»`
|
|
340
399
|
: a.type === 'openUrl' ? `abre ${a.url}`
|
|
341
400
|
: a.type === 'hideEl' ? `oculta ${a.targetId || 'el objeto'}`
|
|
@@ -378,6 +437,78 @@ function resumenDelDoc(doc, { escena, slide } = {}) {
|
|
|
378
437
|
return filas.join('\n') || 'No hay nada con esos números de escena y slide.';
|
|
379
438
|
}
|
|
380
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Varias órdenes en UNA llamada. El gasto de un agente no está en cada orden sino en cada vuelta
|
|
442
|
+
* de pensar: un slide eran ~150 llamadas y, en cada una, el modelo relee toda la conversación.
|
|
443
|
+
* Con una lista, el modelo piensa una vez y el servidor ejecuta. La respuesta es una línea por
|
|
444
|
+
* orden, sin la bitácora, para que tampoco la vuelta engorde.
|
|
445
|
+
*/
|
|
446
|
+
async function tanda(args) {
|
|
447
|
+
const ordenes = Array.isArray(args.ordenes) ? args.ordenes : [];
|
|
448
|
+
if (!ordenes.length) throw new Error('Falta `ordenes`: una lista de { herramienta, args }.');
|
|
449
|
+
if (ordenes.length > MAX_TANDA) throw new Error(`Son ${ordenes.length} órdenes y el máximo son ${MAX_TANDA}. Pártelas en varias tandas.`);
|
|
450
|
+
const filas = [];
|
|
451
|
+
let fallos = 0;
|
|
452
|
+
for (const [i, o] of ordenes.entries()) {
|
|
453
|
+
const h = String(o?.herramienta ?? '').trim();
|
|
454
|
+
try {
|
|
455
|
+
if (!h) throw new Error('falta `herramienta`.');
|
|
456
|
+
if (NO_EN_TANDA.has(h)) throw new Error(`\`${h}\` no va dentro de una tanda: llámala sola.`);
|
|
457
|
+
const dijo = await llamar(h, o.args && typeof o.args === 'object' ? o.args : {});
|
|
458
|
+
filas.push(`${i + 1} ✓ ${h} — ${String(dijo).split('\n(bitácora:')[0].split('\n')[0]}`);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
fallos++;
|
|
461
|
+
filas.push(`${i + 1} ✕ ${h || '?'} — ${e.message}`);
|
|
462
|
+
if (!args.seguir_si_falla) {
|
|
463
|
+
if (i + 1 < ordenes.length) filas.push(`(paré aquí: las ${ordenes.length - i - 1} siguientes NO se hicieron)`);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const texto = filas.join('\n');
|
|
469
|
+
if (fallos) throw new Error(`${fallos} orden(es) fallaron. Lo que salió bien ya está puesto (sin guardar).\n${texto}`);
|
|
470
|
+
return `${ordenes.length} orden(es) hechas.\n${texto}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Una línea por slide: para orientarse sin leer el proyecto entero. */
|
|
474
|
+
function indiceDelDoc(doc) {
|
|
475
|
+
const filas = [];
|
|
476
|
+
doc.forEach((esc, i) => {
|
|
477
|
+
filas.push(`Escena ${i + 1}: «${esc.name ?? 'sin nombre'}»`);
|
|
478
|
+
esc.slides.forEach((s, j) => {
|
|
479
|
+
const n = s.base.length + s.layers.reduce((t, l) => t + l.els.length, 0);
|
|
480
|
+
filas.push(` slide ${j + 1}${s.name ? ` «${s.name}»` : ''} — ${n} objeto(s), ${s.layers.length} capa(s)`);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
return filas.join('\n');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Mueve el punto de trabajo a un slide que YA existe. Antes `abrir_proyecto` dejaba siempre el
|
|
488
|
+
* slide 1 y no había forma de ir a otro: corregir un slide hecho obligaba a rehacer el proyecto
|
|
489
|
+
* (Planificatron, hallazgo 23, 2026-10-12). Números desde 1, como los enseña `resumenDelDoc`.
|
|
490
|
+
*/
|
|
491
|
+
function irASlide(args) {
|
|
492
|
+
const b = yo.borrador;
|
|
493
|
+
const busca = (lista, pide, actual, qué) => {
|
|
494
|
+
if (pide === undefined || pide === null || String(pide).trim() === '') return actual;
|
|
495
|
+
const q = String(pide).trim();
|
|
496
|
+
const n = Number(q);
|
|
497
|
+
if (Number.isInteger(n) && n >= 1 && n <= lista.length) return n - 1;
|
|
498
|
+
const i = lista.findIndex((x) => x.id === q || String(x.name ?? '').trim().toLowerCase() === q.toLowerCase());
|
|
499
|
+
if (i < 0) {
|
|
500
|
+
throw new Error(`No hay ${qué === 'escena' ? 'ninguna escena' : 'ningún slide'} "${q}". `
|
|
501
|
+
+ `${qué === 'escena' ? 'Las' : 'Los'} que hay: ${lista.map((x, k) => `${k + 1} «${x.name ?? ''}»`).join(', ')}.`);
|
|
502
|
+
}
|
|
503
|
+
return i;
|
|
504
|
+
};
|
|
505
|
+
const e = busca(b.doc, args.escena, Math.min(b.escena, b.doc.length - 1), 'escena');
|
|
506
|
+
const s = busca(b.doc[e].slides, args.slide, 0, 'slide');
|
|
507
|
+
b.escena = e;
|
|
508
|
+
b.slide = s;
|
|
509
|
+
return { escena: e + 1, slide: s + 1 };
|
|
510
|
+
}
|
|
511
|
+
|
|
381
512
|
async function llamar(nombre, args = {}) {
|
|
382
513
|
// ── Proyectos ──────────────────────────────────────────────────────────────
|
|
383
514
|
if (nombre === 'listar_proyectos') {
|
|
@@ -410,6 +541,8 @@ async function llamar(nombre, args = {}) {
|
|
|
410
541
|
yo.proyecto = p;
|
|
411
542
|
yo.borrador = nucleo.nuevoBorrador(Array.isArray(p.document) ? p.document : [], 0, 0, '');
|
|
412
543
|
yo.borrador.estricto = true; // sin nadie delante: lo que el Robot resuelve solo, aquí se niega
|
|
544
|
+
const aUnSlide = nombre === 'abrir_proyecto' && (args.slide !== undefined || args.escena !== undefined);
|
|
545
|
+
const dondeTrabajo = aUnSlide ? irASlide({ escena: args.escena, slide: args.slide ?? 1 }) : null;
|
|
413
546
|
yo.borrador.premium = yo.plan === 'pro';
|
|
414
547
|
// `poner_dibujo`, `poner_fondo` y `poner_lottie` buscan en caliente.
|
|
415
548
|
await Promise.all([nucleo.cargarBiblioteca(), nucleo.cargarAnimaciones()]);
|
|
@@ -422,7 +555,23 @@ async function llamar(nombre, args = {}) {
|
|
|
422
555
|
: 'tu espacio Personal';
|
|
423
556
|
return `${nombre === 'crear_proyecto' ? 'Creado' : 'Abierto'} «${p.title}»\nid: ${p.id}\n`
|
|
424
557
|
+ `Dónde: ${nombre === 'crear_proyecto' ? donde : (p.team_id ? `equipo ${p.team_id}` : 'tu espacio Personal')} — se abre desde la galería de https://boosterarena.com\n`
|
|
425
|
-
+ `Plan de la cuenta: ${yo.plan}. ${yo.borrador.doc.length} escena(s).\n
|
|
558
|
+
+ `Plan de la cuenta: ${yo.plan}. ${yo.borrador.doc.length} escena(s).\n`
|
|
559
|
+
+ `Trabajando en: escena ${yo.borrador.escena + 1}, slide ${yo.borrador.slide + 1}.\n\n`
|
|
560
|
+
// Abierto en un slide: el índice de todo + el detalle SOLO de ese. Un proyecto entero con sus
|
|
561
|
+
// ids son miles de tokens que, para corregir un slide, no hacen falta.
|
|
562
|
+
+ (dondeTrabajo
|
|
563
|
+
? `${indiceDelDoc(yo.borrador.doc)}\n\nEl slide de trabajo:\n${resumenDelDoc(yo.borrador.doc, dondeTrabajo)}`
|
|
564
|
+
: resumenDelDoc(yo.borrador.doc));
|
|
565
|
+
}
|
|
566
|
+
if (nombre === 'tanda') {
|
|
567
|
+
exigeProyecto();
|
|
568
|
+
return tanda(args);
|
|
569
|
+
}
|
|
570
|
+
if (nombre === 'ir_a_slide') {
|
|
571
|
+
exigeProyecto();
|
|
572
|
+
const { escena, slide } = irASlide(args);
|
|
573
|
+
return `Ahora trabajas en la escena ${escena}, slide ${slide}. Lo que pongas va aquí; guarda al terminar.\n\n`
|
|
574
|
+
+ resumenDelDoc(yo.borrador.doc, { escena, slide });
|
|
426
575
|
}
|
|
427
576
|
// La voz de Booster: la MISMA función que usa el editor (`/api/voz`, que pone el candado Pro y
|
|
428
577
|
// descuenta las letras en el servidor), luego se sube el MP3 y se planta como un audio normal.
|
|
@@ -522,7 +671,14 @@ ${r.aviso}` : '');
|
|
|
522
671
|
// ── El lienzo: lo hace el MISMO ejecutor que el Robot del navegador ─────────
|
|
523
672
|
exigeProyecto();
|
|
524
673
|
yo.borrador.fallo = false;
|
|
525
|
-
|
|
674
|
+
// Lo que pidió la persona vale SOLO para esta orden: si se quedara puesto, un «quita esto» de
|
|
675
|
+
// hace diez órdenes abriría el cerrojo para cualquier `borrar` posterior.
|
|
676
|
+
yo.borrador.pedido = String(args.pedido_del_maestro ?? '');
|
|
677
|
+
let dijo;
|
|
678
|
+
try { dijo = nucleo.ejecutar(yo.borrador, nombre, args, fabricas()); } finally { yo.borrador.pedido = ''; }
|
|
679
|
+
if (yo.borrador.fallo && !args.pedido_del_maestro && (nombre === 'borrar' || args.accion === 'quitar')) {
|
|
680
|
+
throw new Error(`${dijo} Si la persona SÍ te lo pidió, vuelve a llamar pasando su mensaje tal cual en \`pedido_del_maestro\`.`);
|
|
681
|
+
}
|
|
526
682
|
// ⚠️ UN RECHAZO ES UN ERROR, NO UN ÉXITO CON MALAS NOTICIAS. El ejecutor contesta con una frase
|
|
527
683
|
// también cuando se niega («No existe ninguna arena…»), porque al Robot del navegador le basta
|
|
528
684
|
// leerla. A una fábrica sin nadie delante, no: la contaba como hecha y el slide quedaba vacío
|
package/sesion.mjs
CHANGED
|
@@ -73,12 +73,16 @@ function cliente() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// ── La credencial en disco ───────────────────────────────────────────────────
|
|
76
|
-
// Se guarda el `refresh_token
|
|
77
|
-
//
|
|
76
|
+
// Se guarda el `refresh_token` y, junto a él, el token de acceso con su caducidad. Este archivo
|
|
77
|
+
// ES una llave, así que va a 600 (solo su dueño), igual que `~/.claude`.
|
|
78
|
+
// El de acceso va también al archivo para que dos procesos (el servidor y un `quien` suelto, o
|
|
79
|
+
// dos agentes) se lo PRESTEN en vez de renovar cada uno por su lado.
|
|
78
80
|
function guardar(sesion) {
|
|
79
81
|
if (!existsSync(CARPETA)) mkdirSync(CARPETA, { recursive: true, mode: 0o700 });
|
|
80
82
|
const datos = {
|
|
81
83
|
refresh_token: sesion.refresh_token,
|
|
84
|
+
access_token: sesion.access_token,
|
|
85
|
+
expira: sesion.expires_at ?? Math.floor(Date.now() / 1000) + (sesion.expires_in ?? 3600),
|
|
82
86
|
correo: sesion.user?.email ?? null,
|
|
83
87
|
guardado: new Date().toISOString(),
|
|
84
88
|
};
|
|
@@ -91,15 +95,91 @@ export function leerCredencial() {
|
|
|
91
95
|
try { return JSON.parse(readFileSync(CREDENCIAL, 'utf8')); } catch { return null; }
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
// ── El token de acceso: se RENUEVA SOLO CUANDO VA A CADUCAR ───────────────────
|
|
99
|
+
// ⚠️ Antes se llamaba a `refreshSession` en CADA orden. Un slide con capas son ~150 órdenes, y
|
|
100
|
+
// Supabase limita los refrescos por IP: a mitad de tanda contestaba con error, este código se lo
|
|
101
|
+
// tragaba (`return null`) y el agente veía «No hay sesión» / «ya no vale». Minutos después volvía
|
|
102
|
+
// solo. Lo destapó el agente de una coach fabricando boosters (Planificatron, hallazgo 22,
|
|
103
|
+
// 2026-10-12). Ahora:
|
|
104
|
+
// · el token vive en memoria (y en el archivo) y se reutiliza mientras le queden > 5 minutos;
|
|
105
|
+
// · UN solo refresco a la vez: las órdenes que llegan mientras tanto esperan al mismo;
|
|
106
|
+
// · si el refresco falla, se dice el error REAL de Supabase, no «no hay sesión».
|
|
107
|
+
const MARGEN_S = 5 * 60;
|
|
108
|
+
let enMemoria = null; // { access_token, expira, refresh_token }
|
|
109
|
+
let enCurso = null; // la promesa del refresco que está en marcha, si hay uno
|
|
110
|
+
let refrescos = 0;
|
|
111
|
+
let alRenovarFn = null;
|
|
112
|
+
|
|
113
|
+
/** Para que el servidor apunte en su bitácora cada refresco (debería ser ~1 por hora). */
|
|
114
|
+
export function alRenovar(fn) { alRenovarFn = fn; }
|
|
115
|
+
|
|
116
|
+
const ahoraS = () => Date.now() / 1000;
|
|
117
|
+
const vale = (c) => !!c?.access_token && Number(c.expira) - ahoraS() > MARGEN_S;
|
|
118
|
+
const esperar = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
119
|
+
|
|
120
|
+
function describir(error) {
|
|
121
|
+
if (!error) return 'Supabase no devolvió sesión';
|
|
122
|
+
const partes = [error.message || String(error)];
|
|
123
|
+
if (error.code) partes.push(`[${error.code}]`);
|
|
124
|
+
if (error.status) partes.push(`(HTTP ${error.status})`);
|
|
125
|
+
return partes.join(' ');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function renovar(cred) {
|
|
129
|
+
let ultimo = null;
|
|
130
|
+
let refresh = cred.refresh_token;
|
|
131
|
+
for (let intento = 0; intento < 3; intento++) {
|
|
132
|
+
const { data, error } = await cliente().auth.refreshSession({ refresh_token: refresh });
|
|
133
|
+
if (!error && data?.session) {
|
|
134
|
+
guardar(data.session); // el refresh token ROTA: si no se reescribe, la próxima vez falla
|
|
135
|
+
enMemoria = {
|
|
136
|
+
access_token: data.session.access_token,
|
|
137
|
+
expira: data.session.expires_at,
|
|
138
|
+
refresh_token: data.session.refresh_token,
|
|
139
|
+
};
|
|
140
|
+
refrescos++;
|
|
141
|
+
alRenovarFn?.({ refrescos, expira: new Date(enMemoria.expira * 1000).toISOString() });
|
|
142
|
+
return enMemoria.access_token;
|
|
143
|
+
}
|
|
144
|
+
ultimo = error;
|
|
145
|
+
// ¿Lo renovó otro proceso mientras tanto? Entonces el archivo ya trae uno bueno.
|
|
146
|
+
const otra = leerCredencial();
|
|
147
|
+
if (otra?.refresh_token && otra.refresh_token !== refresh) {
|
|
148
|
+
if (vale(otra)) {
|
|
149
|
+
enMemoria = { access_token: otra.access_token, expira: otra.expira, refresh_token: otra.refresh_token };
|
|
150
|
+
return otra.access_token;
|
|
151
|
+
}
|
|
152
|
+
refresh = otra.refresh_token;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Un 4xx que no es límite de peticiones es definitivo (sesión cerrada, token revocado):
|
|
156
|
+
// reintentar no lo arregla. Red caída, 429 y 5xx sí merecen otra vuelta.
|
|
157
|
+
const st = error?.status;
|
|
158
|
+
if (st && st >= 400 && st < 500 && st !== 429) break;
|
|
159
|
+
await esperar(1000 * 2 ** intento);
|
|
160
|
+
}
|
|
161
|
+
// Si el token que ya tenemos aún no ha caducado del todo, mejor usarlo que tumbar la orden.
|
|
162
|
+
if (cred.access_token && Number(cred.expira) - ahoraS() > 30) return cred.access_token;
|
|
163
|
+
throw new Error(`La sesión de ${cred.correo ?? 'este equipo'} no se pudo renovar: ${describir(ultimo)}. `
|
|
164
|
+
+ 'Si persiste, vuelve a entrar con `npx boosterarena-mcp entrar`.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Devuelve un token de acceso válido, o null si en este equipo no hay sesión.
|
|
169
|
+
* Si hay sesión pero Supabase no la renueva, LANZA con el error real.
|
|
170
|
+
*/
|
|
95
171
|
export async function tokenDeAcceso() {
|
|
96
172
|
const cred = leerCredencial();
|
|
97
|
-
if (!cred?.refresh_token) return null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
173
|
+
if (!cred?.refresh_token) { enMemoria = null; return null; }
|
|
174
|
+
// Otra entrada (u otro proceso que renovó) cambió el archivo: lo de memoria ya no es lo vigente.
|
|
175
|
+
if (enMemoria && enMemoria.refresh_token !== cred.refresh_token) enMemoria = null;
|
|
176
|
+
if (vale(enMemoria)) return enMemoria.access_token;
|
|
177
|
+
if (vale(cred)) {
|
|
178
|
+
enMemoria = { access_token: cred.access_token, expira: Number(cred.expira), refresh_token: cred.refresh_token };
|
|
179
|
+
return cred.access_token;
|
|
180
|
+
}
|
|
181
|
+
enCurso ??= renovar(cred).finally(() => { enCurso = null; });
|
|
182
|
+
return enCurso;
|
|
103
183
|
}
|
|
104
184
|
|
|
105
185
|
// ── Abrir el navegador ───────────────────────────────────────────────────────
|
|
@@ -246,7 +326,8 @@ function escuchar(puerto, estado) {
|
|
|
246
326
|
export async function quien() {
|
|
247
327
|
const cred = leerCredencial();
|
|
248
328
|
if (!cred) { console.log(` No hay sesión en este equipo. Usa: ${YO} entrar`); return; }
|
|
249
|
-
|
|
329
|
+
let token;
|
|
330
|
+
try { token = await tokenDeAcceso(); } catch (e) { console.log(` ✕ ${e.message}`); return; }
|
|
250
331
|
if (!token) { console.log(` La sesión de ${cred.correo ?? 'este equipo'} ya no vale. Vuelve a entrar.`); return; }
|
|
251
332
|
const r = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
|
252
333
|
headers: { apikey: SUPABASE_KEY, Authorization: `Bearer ${token}` },
|