orquestra-mcp 1.16.0 → 1.16.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.16.0",
3
+ "version": "1.16.1",
4
4
  "description": "Servidor MCP de Orquestra -- 89 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,6 +71,11 @@ export const PLATFORM_PLAYBOOKS = {
71
71
  title: 'Eliminar cuenta desde la app (Guideline 5.1.1(v))',
72
72
  body: 'Obligatorio desde 2022: si la app permite crear cuenta, tiene que permitir ELIMINARLA desde DENTRO de la app -- "escríbenos por correo" no cumple el requisito, aunque sea rápido y real. Verifica que exista un flujo de borrado self-service (botón "Eliminar cuenta" en Perfil/Ajustes, con confirmación) ANTES de enviar a revisión -- si no existe, Apple puede rechazar la app durante el review, incluso si el resto de la metadata está perfecta. Orquestra ya tiene este flujo (Zona de riesgo en Perfil, backend server-side que borra cuenta+datos), úsalo de referencia de implementación si tu app lo necesita desde cero.',
73
73
  },
74
+ {
75
+ key: 'sign_in_apple',
76
+ title: 'Sign in with Apple si hay login de terceros (Guideline 4.8)',
77
+ body: 'Si la app ofrece login con Google, Facebook u otro proveedor de terceros, Apple exige que TAMBIÉN ofrezcas "Sign in with Apple" como alternativa equivalente -- Guideline 4.8 ("Login Services"). No es una recomendación: si falta, Apple RECHAZA el build completo en revisión (motivo real, no hipotético) sin importar que el resto de la metadata esté perfecta. Implementación de referencia: botón nativo (`AuthenticationServices`, nonce) + capability "Sign in with Apple" habilitada en el App ID (Apple Developer Portal) + provider "Apple" activado en Firebase Auth si se usa Firebase. Si el login es solo email/password propio, sin ningún proveedor de terceros, esta regla no aplica.',
78
+ },
74
79
  ],
75
80
 
76
81
  tasks: [
@@ -114,6 +119,11 @@ export const PLATFORM_PLAYBOOKS = {
114
119
  description: 'Guideline 5.1.1(v), obligatorio -- "escríbenos por correo" NO cumple el requisito. Tiene que haber un botón real de "Eliminar cuenta" (con confirmación) en Perfil/Ajustes que borre la cuenta y los datos sin salir de la app. Si no existe, constrúyelo ANTES de mandar a revisión, no después de un rechazo.',
115
120
  type: 'security', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 1,
116
121
  },
122
+ {
123
+ title: 'Verificar que si hay login de terceros (Google/Facebook/etc.), también esté "Sign in with Apple"',
124
+ description: 'Guideline 4.8, obligatorio -- si la app ofrece login con Google, Facebook u otro proveedor externo, Apple rechaza la app en revisión si no ofrece también "Sign in with Apple" como alternativa. No aplica si el login es solo email/password propio, sin proveedores de terceros. Si falta, agrégalo ANTES de mandar a revisión, no después de un rechazo.',
125
+ type: 'security', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 1,
126
+ },
117
127
  ],
118
128
  },
119
129
 
@@ -190,6 +200,11 @@ export const PLATFORM_PLAYBOOKS = {
190
200
  title: 'App access / Sign-in details',
191
201
  body: 'Si la app requiere login, "Add sign in details" pide un set de credenciales de prueba REALES (nombre descriptivo del set, usuario/email, contraseña) que el reviewer va a usar de verdad para entrar -- tienen que funcionar y, si es posible, mostrar contenido de ejemplo real (no una cuenta vacía cayendo en un onboarding). Marca el checkbox de "esta cuenta da acceso completo a todas las funciones" si no hay niveles pagos/premium.',
192
202
  },
203
+ {
204
+ key: 'google_signin_sha1',
205
+ title: 'Google Sign-In: SHA-1 de las TRES firmas en Firebase (debug, upload key y Play App Signing)',
206
+ body: 'Si la app tiene "Continuar con Google", Google valida la firma del APK contra los SHA-1 registrados en Firebase → Configuración del proyecto → app Android → "Agregar huella digital". Son TRES firmas distintas y cada una necesita su SHA-1: (1) debug (`~/.android/debug.keystore`, builds locales), (2) upload key (el keystore con el que firmas el AAB, `assembleRelease`), y (3) Play App Signing -- la clave con la que Google RE-FIRMA los APK que instalan testers y usuarios desde Play; NO es la upload key, y es la que siempre se olvida. Se ve en Play Console → Test and release → App integrity → Play Store protection → Play app signing → "App signing key certificate" (columna Classical key). Las dos primeras salen de `./gradlew signingReport`. Síntoma si falta alguna: al elegir la cuenta "no pasa nada" -- Play Services lo reporta como cancelación del usuario, no como error de configuración, así que no hay ningún mensaje que te avise. Después de registrarlas, vuelve a descargar google-services.json.',
207
+ },
193
208
  {
194
209
  key: 'delete_account',
195
210
  title: 'Eliminar cuenta desde la app (política de borrado de cuenta de Google Play)',
@@ -228,6 +243,11 @@ export const PLATFORM_PLAYBOOKS = {
228
243
  description: 'Política de borrado de cuenta de Google Play (equivalente a la Guideline 5.1.1(v) de Apple) -- lo ideal es un flujo real dentro de la app, no solo la "Delete account URL" de la ficha. Si no existe, constrúyelo antes de publicar.',
229
244
  type: 'security', priority: 'high', tags: ['publicacion', 'android', 'compliance'], offsetDays: 1,
230
245
  },
246
+ {
247
+ title: 'Registrar en Firebase el SHA-1 de las TRES firmas (debug, upload key y Play App Signing) si la app usa Google Sign-In',
248
+ description: 'Gotcha real: cada firma distinta del APK necesita su propio SHA-1 en Firebase (Configuración del proyecto → app Android → "Agregar huella digital"), y la que siempre se olvida es la de Play App Signing -- Google re-firma con ella los APK que se instalan desde Play, así que un login con Google que funciona en debug y en el release local falla en el build de los testers sin ningún mensaje (Play Services lo reporta como cancelación). Debug y upload key: `./gradlew signingReport`. Play App Signing: Play Console → App integrity → Play app signing → "App signing key certificate". Al terminar, re-descargar google-services.json.',
249
+ type: 'tech', priority: 'high', tags: ['publicacion', 'android', 'google-sign-in'], offsetDays: 1,
250
+ },
231
251
  {
232
252
  title: 'Preparar capturas de pantalla en los tamaños exactos que pide Play Console',
233
253
  description: 'Mínimo 2 capturas de teléfono, más el gráfico destacado (1024x500) y el ícono (512x512) -- Play Console valida las dimensiones al subir, ajusta según lo que rechace.',
package/src/server.mjs CHANGED
@@ -1589,6 +1589,8 @@ export function createServer() {
1589
1589
  saves: z.number().optional().describe('Guardados -- entrada manual, típico de X/Instagram (solo con channelId suelto)'),
1590
1590
  avgReadMinutes: z.number().optional().describe('Minutos de lectura media -- entrada manual, típico de blog (solo con channelId suelto)'),
1591
1591
  imagePrompt: z.string().optional().describe('Descripción visual en inglés para generar la portada -- no genera la imagen, solo guarda el texto (solo con channelId suelto)'),
1592
+ recommendedMedia: z.enum(['image', 'video']).optional().describe('Qué conviene más de portada para esta pieza -- puramente informativo, ningún flujo cambia solo por esto (solo con channelId suelto)'),
1593
+ videoPrompt: z.string().optional().describe('Análogo a imagePrompt pero para video: descripción de la escena en movimiento, en inglés, para pegar en un generador de video externo -- Orquestra no genera video, solo guarda el texto (solo con channelId suelto)'),
1592
1594
  language: z.string().optional().describe('\'en\' si esta pieza es la traducción de otra. Sin valor = idioma original.'),
1593
1595
  translationOf: z.string().optional().describe('Si esta pieza es una traducción, id del content original'),
1594
1596
  })
@@ -1616,6 +1618,8 @@ export function createServer() {
1616
1618
  saves: z.number().optional(),
1617
1619
  avgReadMinutes: z.number().optional(),
1618
1620
  imagePrompt: z.string().optional(),
1621
+ recommendedMedia: z.enum(['image', 'video']).optional(),
1622
+ videoPrompt: z.string().optional(),
1619
1623
  language: z.string().optional(),
1620
1624
  translationOf: z.string().optional(),
1621
1625
  })
@@ -1812,7 +1816,7 @@ export function createServer() {
1812
1816
  server.registerTool(
1813
1817
  'apply_platform_playbook',
1814
1818
  {
1815
- description: 'Aplica el Platform Playbook de publicación (iOS App Store / Google Play) sobre una distribución real: crea el módulo + tareas de la plantilla (igual que apply_stage_playbook) y, si mandás publishForm, también completa el formulario de la Guía de publicación de esa distribución (mismo dato que llena DistributionGuide.jsx en la web). Solo completá campos para los que tengas una base real del proyecto (código, get_project_context, servicios/entornos configurados) -- nunca inventes pricing, availability, package/bundle ID, cuentas demo para el reviewer, o el nombre exacto de la app; dejalos afuera de publishForm para que el usuario los complete a mano. Si la distribución YA tiene valores guardados en alguna de las claves que mandás, la tool NO los sobreescribe en silencio -- devuelve publishFormConflict con el valor actual vs. el propuesto por cada clave en conflicto, sin tocar publishForm. Mostrale ese conflicto al usuario y volvé a llamar con confirmReplace:true SOLO si confirma explícitamente el reemplazo.',
1819
+ description: 'Aplica el Platform Playbook de publicación (iOS App Store / Google Play) sobre una distribución real: crea el módulo + tareas de la plantilla (igual que apply_stage_playbook) y, si mandás publishForm, también completa el formulario de la Guía de publicación de esa distribución (mismo dato que llena DistributionGuide.jsx en la web). Solo completá campos para los que tengas una base real del proyecto (código, get_project_context, servicios/entornos configurados) -- nunca inventes pricing, availability, package/bundle ID, cuentas demo para el reviewer, o el nombre exacto de la app; dejalos afuera de publishForm para que el usuario los complete a mano. Si la distribución YA tiene valores guardados en alguna de las claves que mandás, la tool NO los sobreescribe en silencio -- devuelve publishFormConflict con el valor actual vs. el propuesto por cada clave en conflicto, sin tocar publishForm. Mostrale ese conflicto al usuario y volvé a llamar con confirmReplace:true SOLO si confirma explícitamente el reemplazo. Si este playbook YA se aplicó antes sobre esta misma distribución (módulo+tareas ya creados), la tool lo detecta sola (mismo mecanismo que el badge "Ya aplicado" de DistributionGuide.jsx) y NO vuelve a crear módulo/tareas duplicados -- devuelve alreadyApplied:true. Volvé a llamar con force:true SOLO si el usuario pide explícitamente un checklist nuevo encima del que ya existe.',
1816
1820
  inputSchema: {
1817
1821
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1818
1822
  platform: z.enum(['ios', 'android']).describe('ios (App Store Connect) | android (Google Play Console) -- ver get_platform_playbook para el contenido'),
@@ -1820,40 +1824,58 @@ export function createServer() {
1820
1824
  moduleName: z.string().optional().describe('Nombre del módulo -- default: el de la plantilla'),
1821
1825
  publishForm: z.record(z.string()).optional().describe('Respuestas a completar, clave = key de referenceGuide (ver get_platform_playbook). Omitido o vacío = solo crea módulo + tareas, sin tocar el formulario.'),
1822
1826
  confirmReplace: z.boolean().optional().describe('Default false. Mandá true solo cuando el usuario ya confirmó explícitamente reemplazar valores existentes que te devolvió publishFormConflict en un llamado anterior.'),
1827
+ force: z.boolean().optional().describe('Default false. Si este playbook ya se aplicó antes sobre esta distribución, por default la tool NO crea un segundo módulo/checklist -- mandá true solo cuando el usuario pidió explícitamente crear uno nuevo encima del que ya existe.'),
1823
1828
  },
1824
1829
  },
1825
- async ({ projectId, platform, distributionId, moduleName, publishForm, confirmReplace }) => {
1830
+ async ({ projectId, platform, distributionId, moduleName, publishForm, confirmReplace, force }) => {
1826
1831
  try {
1827
1832
  const pid = resolveProjectId(projectId)
1828
1833
  const playbook = PLATFORM_PLAYBOOKS[platform]
1829
- const moduleData = await apiPost(config, '/v1/modules', {
1830
- projectId: pid,
1831
- name: moduleName || playbook.moduleName,
1832
- description: playbook.moduleDescription,
1833
- tags: playbook.moduleTags,
1834
- })
1834
+ const appliedKey = `${distributionId}_publish`
1835
1835
 
1836
- const taskIds = []
1837
- for (const t of playbook.tasks) {
1838
- const dueDate = new Date(Date.now() + t.offsetDays * 86400000).toISOString().slice(0, 10)
1839
- const created = await apiPost(config, '/v1/tasks', {
1836
+ const project = await apiGet(config, `/v1/projects/${pid}`, {})
1837
+ const alreadyApplied = !!(project.appliedPlaybooks || {})[appliedKey]
1838
+
1839
+ const result = {}
1840
+
1841
+ if (alreadyApplied && !force) {
1842
+ result.alreadyApplied = true
1843
+ result.message = 'Este playbook ya se había aplicado antes sobre esta distribución (el módulo y sus tareas ya existen en el proyecto) -- no se creó nada de nuevo para no duplicarlo. Si el usuario pide explícitamente un checklist nuevo, volvé a llamar con force:true.'
1844
+ } else {
1845
+ const moduleData = await apiPost(config, '/v1/modules', {
1840
1846
  projectId: pid,
1841
- title: t.title,
1842
- description: t.description,
1843
- type: t.type,
1844
- priority: t.priority,
1845
- tags: t.tags,
1846
- moduleId: moduleData.id,
1847
- dueDate,
1847
+ name: moduleName || playbook.moduleName,
1848
+ description: playbook.moduleDescription,
1849
+ tags: playbook.moduleTags,
1848
1850
  })
1849
- taskIds.push(created.id)
1850
- }
1851
1851
 
1852
- const result = { module: moduleData, tasksCreated: taskIds.length, taskIds }
1852
+ const taskIds = []
1853
+ for (const t of playbook.tasks) {
1854
+ const dueDate = new Date(Date.now() + t.offsetDays * 86400000).toISOString().slice(0, 10)
1855
+ const created = await apiPost(config, '/v1/tasks', {
1856
+ projectId: pid,
1857
+ title: t.title,
1858
+ description: t.description,
1859
+ type: t.type,
1860
+ priority: t.priority,
1861
+ tags: t.tags,
1862
+ moduleId: moduleData.id,
1863
+ dueDate,
1864
+ })
1865
+ taskIds.push(created.id)
1866
+ }
1867
+
1868
+ result.module = moduleData
1869
+ result.tasksCreated = taskIds.length
1870
+ result.taskIds = taskIds
1871
+
1872
+ await apiPatch(config, '/v1/projects', { id: pid, appliedPlaybooks: { [appliedKey]: true } })
1873
+ result.markedApplied = true
1874
+ }
1853
1875
 
1854
1876
  if (publishForm && Object.keys(publishForm).length > 0) {
1855
1877
  const distributions = await apiGet(config, '/v1/distributions', { projectId: pid })
1856
- const dist = distributions.find(d => d.id === distributionId)
1878
+ const dist = distributions.items.find(d => d.id === distributionId)
1857
1879
  if (!dist) {
1858
1880
  throw new Error(`No se encontró la distribución "${distributionId}" en este proyecto -- revisá list_distributions.`)
1859
1881
  }