boosterarena-mcp 0.3.4 → 0.3.6

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 CHANGED
@@ -20,7 +20,7 @@ Hace falta **Node 20 o más nuevo**.
20
20
  npx boosterarena-mcp entrar
21
21
  ```
22
22
 
23
- Te pregunta cómo entras: Enter para Google, o tu correo si tu cuenta es de correo y contraseña (te llega un enlace). También directo:
23
+ Te pregunta cómo entras: Enter para Google, o tu correo si tu cuenta es de correo y contraseña (te llega un enlace). Si lo corre un agente (sin terminal), **tiene que pasar el correo**: sin `--correo` se detiene y nunca abre Google. Directo:
24
24
  `npx boosterarena-mcp entrar --correo tu@correo.com`.
25
25
 
26
26
  La credencial se guarda en `~/.booster/credenciales.json`, solo en tu equipo. Para comprobar a
@@ -57,7 +57,7 @@ borrador y tú decides cuándo se publica.
57
57
 
58
58
  **Contenido** — poner texto, imágenes, figuras y dibujos de la biblioteca (más de 3.000
59
59
  ilustraciones y 280 fondos, que se buscan por nombre en español o inglés), y animaciones Lottie
60
- (`poner_lottie`: de la galería de Booster o por URL). El fondo puede ser una imagen tuya
60
+ (`poner_lottie`: de la galería de Booster o por URL) y videos de YouTube, Vimeo, Drive o Loom (`poner_video`, con el enlace normal). Con `exacto_o_nada` en `poner_lottie`/`poner_dibujo` no se conforma con un parecido. `cambiar_estilo` pone a cualquier objeto una ENTRADA al aparecer (`entrada`: pop, bounce, zoom…, con `entrada_retraso`) y a imágenes, animaciones, emojis y figuras un MOVIMIENTO en bucle (`movimiento`: float, bounce, swing…). El fondo puede ser una imagen tuya
61
61
  (`poner_fondo` con `url`), y cada capa puede llevar el suyo (`capa` en `poner_fondo`, o `fondo`
62
62
  en `nueva_capa`).
63
63
 
package/cli.mjs CHANGED
@@ -25,7 +25,7 @@ function ayuda() {
25
25
  console.log(`
26
26
  El MCP de Booster — tu agente trabajando dentro de boosterarena.com
27
27
 
28
- boosterarena-mcp entrar te pregunta si entras con Google o con correo
28
+ boosterarena-mcp entrar te pregunta si entras con Google o con correo (sin terminal: exige --correo)
29
29
  boosterarena-mcp entrar --correo tu@correo entrar con un enlace por correo
30
30
  boosterarena-mcp quien a nombre de quién estoy
31
31
  boosterarena-mcp salir borrar la credencial de este equipo
package/dist/nucleo.mjs CHANGED
@@ -1647,7 +1647,9 @@ function construirCatalogo(opts) {
1647
1647
  tamano: num("Tama\xF1o de letra en p\xEDxeles sobre 1080."),
1648
1648
  efecto: str("Efecto de COLOR de las letras. Ojo: el ne\xF3n pinta las letras del color DEL EFECTO, as\xED que \xABtexto blanco con ne\xF3n\xBB es color blanco Y color_efecto blanco.", { enum: efectosColor }),
1649
1649
  color_efecto: str("Color del efecto en hex: el brillo del ne\xF3n, la sombra, el borde, el bloque. Si no lo mandas, cada efecto usa el suyo."),
1650
- movimiento: str("Efecto de MOVIMIENTO.", { enum: efectosMov }),
1650
+ movimiento: str("Efecto de MOVIMIENTO en bucle. En textos, las letras. En im\xE1genes, animaciones, emojis y figuras, el objeto entero en el \u25B6 Player: float, bounce, pulse, heartbeat, swing, glide, shake, orbit, elastic (wave y sweep son solo de texto). none lo quita.", { enum: efectosMov }),
1651
+ entrada: str("Animaci\xF3n de ENTRADA del objeto al aparecer en el \u25B6 Player (al empezar el slide o al abrirse su capa). none la quita.", { enum: ["none", "pop", "bounce", "zoom", "fade", "up", "drop", "left", "right", "spin"] }),
1652
+ entrada_retraso: num("Segundos de espera antes de la entrada (0\u201330), para que los objetos entren uno tras otro: 0, 0.4, 0.8\u2026"),
1651
1653
  peso: num("Grosor de la letra, 400 a 900."),
1652
1654
  fuente: FUENTE,
1653
1655
  mayusculas: bool("TODO EN MAY\xDASCULAS (o false para quitarlo)."),
@@ -1723,6 +1725,7 @@ function conLoDelMcp(lista2, fn) {
1723
1725
  recolocar.y = num("Nuevo borde superior en %. Tambi\xE9n puede quedar fuera del slide, como `x`.");
1724
1726
  props("poner_figura").alto = num("Alto en % del alto del slide, independiente del ancho. Para los paneles de capa: una elipse blanca de w 82 \xD7 alto 77, o un rect de 83 \xD7 71. Sin esto, la figura guarda su proporci\xF3n de siempre.");
1725
1727
  recolocar.alto = num("Nuevo alto en %: para cajas (Revelador, Ciclo, QR\u2026), zonas calientes y FIGURAS, independiente del ancho.");
1728
+ props("poner_dibujo").exacto_o_nada = bool("true = si la biblioteca no tiene uno que sea EXACTAMENTE lo que buscas, no pone nada y te dice los parecidos. Sin esto se pone el m\xE1s cercano.");
1726
1729
  props("cambiar_estilo").gris_al_tocar = bool("true = en el \u25B6 Player, al tocar el objeto se vuelve gris (y otro toque le devuelve el color). Le dice al coach qu\xE9 botones ya us\xF3. false lo quita.");
1727
1730
  const i = lista2.findIndex((t) => t.function.name === "poner_emoji");
1728
1731
  lista2.splice(i + 1, 0, fn(
@@ -1735,9 +1738,22 @@ function conLoDelMcp(lista2, fn) {
1735
1738
  w: W,
1736
1739
  capa: CAPA,
1737
1740
  busca: str('Qu\xE9 animaci\xF3n buscar en la galer\xEDa, en espa\xF1ol, ingl\xE9s o portugu\xE9s: "regalo", "trofeo", "estrella", "altavoz".'),
1738
- url: str("En vez de `busca`: la URL https:// de un Lottie .json (lottie.host, o uno subido).")
1741
+ url: str("En vez de `busca`: la URL https:// de un Lottie .json (lottie.host, o uno subido)."),
1742
+ exacto_o_nada: bool("true = si no hay uno que sea EXACTAMENTE lo que buscas, no pone nada y te dice los parecidos, para que elijas otra palabra. Sin esto se pone el m\xE1s cercano.")
1739
1743
  }, ["ref", "x", "y", "w"])
1740
1744
  ));
1745
+ lista2.splice(i + 2, 0, fn(
1746
+ "poner_video",
1747
+ "Pone un VIDEO embebido en el slide: el enlace normal de YouTube (watch, youtu.be, shorts), Vimeo, Google Drive, Loom o Dailymotion, o un .mp4 subido. Se convierte solo al formato embebido y entra en 16:9. El maestro le da play en el \u25B6 Player. No lo cambies por un QR ni un bot\xF3n.",
1748
+ obj({
1749
+ ref: REF,
1750
+ x: X,
1751
+ y: Y,
1752
+ w: W,
1753
+ capa: CAPA,
1754
+ url: str("El enlace del video tal como lo copias del navegador (https://www.youtube.com/watch?v=\u2026), o el c\xF3digo <iframe> del sitio.")
1755
+ }, ["ref", "x", "y", "w", "url"])
1756
+ ));
1741
1757
  return lista2;
1742
1758
  }
1743
1759
 
@@ -1821,6 +1837,31 @@ function abreEnBucle(slide, desde, t) {
1821
1837
  return false;
1822
1838
  }
1823
1839
 
1840
+ // ../src/canvas/videoSrc.ts
1841
+ function parseVideoInput(code) {
1842
+ const t = code.trim();
1843
+ const fromIframe = t.match(/<iframe[^>]*\ssrc=["']([^"']+)["']/i)?.[1];
1844
+ const raw = fromIframe ?? (/^https?:\/\//i.test(t) ? t : null);
1845
+ if (!raw) return null;
1846
+ let m = raw.match(/(?:youtube(?:-nocookie)?\.com\/(?:watch\?[^#]*?v=|shorts\/|live\/|embed\/)|youtu\.be\/)([\w-]{6,})/i);
1847
+ if (m) {
1848
+ const start = raw.match(/[?&](?:t|start)=(\d+)/)?.[1];
1849
+ return `https://www.youtube.com/embed/${m[1]}${start ? `?start=${start}` : ""}`;
1850
+ }
1851
+ m = raw.match(/vimeo\.com\/(?:video\/)?(\d+)(?:\/([a-f0-9]+))?/i);
1852
+ if (m) {
1853
+ const h = m[2] ?? raw.match(/[?&]h=([a-z0-9]+)/i)?.[1];
1854
+ return `https://player.vimeo.com/video/${m[1]}${h ? `?h=${h}` : ""}`;
1855
+ }
1856
+ m = raw.match(/drive\.google\.com\/file\/d\/([\w-]+)/i);
1857
+ if (m) return `https://drive.google.com/file/d/${m[1]}/preview`;
1858
+ m = raw.match(/loom\.com\/(?:share|embed)\/([a-f0-9]+)/i);
1859
+ if (m) return `https://www.loom.com/embed/${m[1]}`;
1860
+ m = raw.match(/dailymotion\.com\/video\/(\w+)/i);
1861
+ if (m) return `https://www.dailymotion.com/embed/video/${m[1]}`;
1862
+ return raw;
1863
+ }
1864
+
1824
1865
  // ../src/shared/fontsAncho.ts
1825
1866
  var ANCHO_LETRA = {
1826
1867
  "Poppins": { 100: [712, 689], 200: [724, 692], 300: [736, 696], 400: [747, 699], 500: [760, 703], 600: [770, 708], 700: [782, 715], 800: [793, 723], 900: [805, 731] },
@@ -3361,6 +3402,10 @@ El ref "${refBase}" es el rengl\xF3n grande; cada uno es tambi\xE9n "${refBase}_
3361
3402
  if (!hallado) {
3362
3403
  return no(b, `La biblioteca no tiene nada parecido a "${busca}". Prueba con otra palabra m\xE1s com\xFAn y en singular, o resu\xE9lvelo con una figura o un emoji.`);
3363
3404
  }
3405
+ if (args.exacto_o_nada === true && !hallado.exacto) {
3406
+ const par = [hallado.nombre, ...hallado.parecidos].map((x) => `"${x}"`).join(", ");
3407
+ return no(b, `No puse nada: la biblioteca no tiene un dibujo que sea "${busca}". Lo m\xE1s parecido: ${par}. Prueba otra palabra, o vuelve a pedirlo sin \`exacto_o_nada\` si alguno te sirve.`);
3408
+ }
3364
3409
  const pedido = pct(args.w, 20);
3365
3410
  const caja = cajaImagen(pedido, hallado.aspect);
3366
3411
  const el = encajar({
@@ -3417,6 +3462,10 @@ El ref "${refBase}" es el rengl\xF3n grande; cada uno es tambi\xE9n "${refBase}_
3417
3462
  if (!url && !hallada) {
3418
3463
  return no(b, `La galer\xEDa de animaciones no tiene nada parecido a "${busca}" (o no contest\xF3). Prueba otra palabra, en singular, o pon un dibujo con \`poner_dibujo\`.`);
3419
3464
  }
3465
+ if (hallada && args.exacto_o_nada === true && !hallada.exacto) {
3466
+ const par = [hallada.nombre, ...hallada.parecidos].map((x) => `"${x}"`).join(", ");
3467
+ return no(b, `No puse nada: la galer\xEDa no tiene una animaci\xF3n que sea "${busca}". Lo m\xE1s parecido: ${par}. Prueba otra palabra, o vuelve a pedirla sin \`exacto_o_nada\` si alguna te sirve.`);
3468
+ }
3420
3469
  if (hallada?.plan === "pro" && !b.premium) {
3421
3470
  return no(b, `La animaci\xF3n \xAB${hallada.nombre}\xBB es del plan Pro y esta cuenta no lo tiene. Se activa desde boosterarena.com.`);
3422
3471
  }
@@ -3437,6 +3486,26 @@ El ref "${refBase}" es el rengl\xF3n grande; cada uno es tambi\xE9n "${refBase}_
3437
3486
  const otros = hallada.parecidos.length ? ` Otras parecidas: ${hallada.parecidos.map((x) => `"${x}"`).join(", ")}.` : "";
3438
3487
  return hallada.exacto ? `Animaci\xF3n "${hallada.nombre}" puesta (id ${el.id}, exacto: true).${otros}` : `Animaci\xF3n "${hallada.nombre}" puesta (id ${el.id}, exacto: false). \u26A0\uFE0F OJO: no hay ninguna que sea "${busca}"; es la m\xE1s cercana.${otros}`;
3439
3488
  }
3489
+ // Solo lo ofrece el MCP. Mismo elemento que Insertar → Video del editor.
3490
+ case "poner_video": {
3491
+ const src = parseVideoInput(txt(args.url));
3492
+ if (!src) {
3493
+ return no(b, "La `url` tiene que ser el enlace https:// del video (YouTube, Vimeo, Drive, Loom, Dailymotion o un .mp4 subido) o el c\xF3digo <iframe> del sitio.");
3494
+ }
3495
+ const w = pct(args.w, 44);
3496
+ const el = encajar({
3497
+ id: nid("video"),
3498
+ kind: "video",
3499
+ x: pct(args.x, 28),
3500
+ y: pct(args.y, 24),
3501
+ w,
3502
+ h: w,
3503
+ src
3504
+ });
3505
+ const { dentroDe } = plantar(b, el, args.ref, args.capa);
3506
+ b.bitacora.push(`\u{1F3AC} Video${dentroDe ? ` en la capa \xAB${dentroDe}\xBB` : ""}`);
3507
+ return `Video puesto (id ${el.id}, ${src}).`;
3508
+ }
3440
3509
  case "poner_audio": {
3441
3510
  const url = txt(args.url).trim();
3442
3511
  if (!/^https?:\/\//i.test(url)) {
@@ -3803,7 +3872,9 @@ Pide \`describir("${el.widget}")\` para ver la forma exacta y vuelve a intentarl
3803
3872
  }
3804
3873
  if (txt(args.efecto)) el.effect = txt(args.efecto) === "none" ? void 0 : txt(args.efecto);
3805
3874
  Object.assign(el, colorDelEfecto(el.effect ?? "", txt(args.color_efecto), txt(args.color)));
3806
- if (txt(args.movimiento)) el.motion = txt(args.movimiento);
3875
+ if (txt(args.movimiento)) el.motion = txt(args.movimiento) === "none" && el.kind !== "text" ? void 0 : txt(args.movimiento);
3876
+ if (txt(args.entrada)) el.entrada = txt(args.entrada) === "none" ? void 0 : txt(args.entrada);
3877
+ if (typeof args.entrada_retraso === "number") el.entradaRetraso = Math.max(0, Math.min(30, args.entrada_retraso)) || void 0;
3807
3878
  if (num3(args.peso, 0) >= 400) el.weight = pesoReal(el.font, Math.round(num3(args.peso, 400)));
3808
3879
  if (typeof args.mayusculas === "boolean") el.caps = args.mayusculas || void 0;
3809
3880
  if (txt(args.align)) el.align = txt(args.align);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boosterarena-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "El MCP de Booster: tu agente crea y edita presentaciones dentro de boosterarena.com",
5
5
  "keywords": [
6
6
  "mcp",
package/sesion.mjs CHANGED
@@ -199,11 +199,17 @@ function abrirNavegador(url) {
199
199
  * CINCO MINUTOS esperando hasta «Nadie volvió del navegador», sin decir por qué (2026-09-16). De 8
200
200
  * cuentas, 5 son de correo: Google por defecto era la opción equivocada para la mayoría.
201
201
  *
202
- * Solo se pregunta con una persona delante (terminal). Sin terminal no hay a quién preguntarle, y
203
- * se sigue con Google como antes, pero avisando de la salida.
202
+ * Solo se pregunta con una persona delante (terminal). Sin terminal (lo corre un AGENTE) NO se
203
+ * abre Google: un agente lo corría a secas, caía en Google y le abría a la coach una pestaña que su
204
+ * cuenta de correo nunca iba a pasar (2026-10-13). Se corta con la instrucción exacta.
205
+ * Con una persona delante se sigue preguntando (Enter = Google).
204
206
  */
205
207
  async function elegirPuerta() {
206
- if (!process.stdin.isTTY) return null;
208
+ if (!process.stdin.isTTY) {
209
+ throw new Error('Falta el correo. Pregúntale a la persona con qué CORREO entra a boosterarena.com y corre: '
210
+ + '`npx -y boosterarena-mcp entrar --correo <su-correo>`. Le llega un enlace de noreply@boosterarena.com '
211
+ + 'que tiene que abrir EN ESTE EQUIPO. No abras Google ni otra pestaña de inicio de sesión.');
212
+ }
207
213
  const rl = createInterface({ input: process.stdin, output: process.stdout });
208
214
  try {
209
215
  console.log('\n ¿Cómo entras a boosterarena.com?');