orquestra-mcp 1.43.1 → 1.46.0
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 +2 -2
- package/src/platformPlaybooks.mjs +192 -44
- package/src/server.mjs +45 -7
- package/src/setup.mjs +66 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Servidor MCP de Orquestra -- 135 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": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://orquestra.me/mcp",
|
|
18
|
+
"homepage": "https://www.orquestra.me/mcp",
|
|
19
19
|
"keywords": [
|
|
20
20
|
"mcp",
|
|
21
21
|
"model-context-protocol",
|
|
@@ -19,74 +19,177 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
19
19
|
moduleDescription: 'Checklist verificable para enviar la app a revisión en App Store Connect, sin sorpresas de último momento.',
|
|
20
20
|
moduleTags: ['playbook', 'publicacion'],
|
|
21
21
|
|
|
22
|
+
// Revisado contra App Store Connect real el 19 oct 2026 (ORQ-383). `section`
|
|
23
|
+
// agrupa la guía en el mismo orden que el menú de la consola; las keys son
|
|
24
|
+
// estables (viven en distributions.publishForm), no se renombran.
|
|
22
25
|
referenceGuide: [
|
|
26
|
+
// ── App Information ──
|
|
23
27
|
{
|
|
24
|
-
key: 'name',
|
|
25
|
-
title: 'Name',
|
|
26
|
-
body: 'El nombre de marca tal cual. Apple no permite keyword-stuffing en el nombre desde 2017.',
|
|
28
|
+
key: 'name', section: 'App Information',
|
|
29
|
+
title: 'Name (máx. 30 caracteres)',
|
|
30
|
+
body: 'El nombre de marca tal cual. Apple no permite keyword-stuffing en el nombre desde 2017. Es común a todas las versiones (se edita en App Information, no en la versión).',
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
|
-
key: 'subtitle',
|
|
33
|
+
key: 'subtitle', section: 'App Information',
|
|
30
34
|
title: 'Subtitle (máx. 30 caracteres)',
|
|
31
|
-
body: 'Una frase corta que diga a quién sirve y qué resuelve, no un slogan genérico. Cuenta caracteres literal.',
|
|
35
|
+
body: 'Una frase corta que diga a quién sirve y qué resuelve, no un slogan genérico. Cuenta caracteres literal. Las palabras del subtítulo ya cuentan para búsqueda: no las repitas en Keywords.',
|
|
32
36
|
},
|
|
33
37
|
{
|
|
34
|
-
key: 'category',
|
|
35
|
-
title: 'Category',
|
|
36
|
-
body: '
|
|
38
|
+
key: 'category', section: 'App Information',
|
|
39
|
+
title: 'Category (Primary + Secondary)',
|
|
40
|
+
body: 'La primaria es la que más pesa en búsqueda y rankings: elige la del uso principal del usuario final (ej. Productivity para gestión de proyectos), y la secundaria para el caso de nicho (ej. Developer Tools si también integra repos/QA). Si la app ES una herramienta del ciclo de vida de software, Developer Tools va de primaria.',
|
|
37
41
|
},
|
|
38
42
|
{
|
|
39
|
-
key: 'content_rights',
|
|
43
|
+
key: 'content_rights', section: 'App Information',
|
|
40
44
|
title: 'Content Rights',
|
|
41
45
|
body: 'Pregunta si la app contiene/muestra/accede a contenido de terceros. Apunta a apps que REDISTRIBUYEN contenido con derechos de terceros (streaming, stock media) -- no a "cualquier dato que pase por la app". Si solo se muestran datos propios del usuario/equipo (o conectados desde su propia cuenta, ej. su propio repo de GitHub), la respuesta es "No".',
|
|
42
46
|
},
|
|
43
47
|
{
|
|
44
|
-
key: 'age_features',
|
|
48
|
+
key: 'age_features', section: 'App Information',
|
|
45
49
|
title: 'Age Ratings — Paso 1 (Features)',
|
|
46
|
-
body: 'Formulario sobre controles/capacidades técnicas (Parental Controls, Age Assurance, Unrestricted Web Access, User-Generated Content, Social Media, Messaging and Chat, Advertising). Ojo con "User-Generated Content": la definición de Apple es sobre "broad distribution" (distribución amplia/pública) -- si el contenido queda dentro de un workspace/equipo privado entre miembros invitados, NO cuenta como UGC para esta pregunta. Para una app B2B típica todo va en "No".',
|
|
50
|
+
body: 'Formulario sobre controles/capacidades técnicas (Parental Controls, Age Assurance, Unrestricted Web Access, User-Generated Content, Social Media, Messaging and Chat, Advertising). Ojo con "User-Generated Content": la definición de Apple es sobre "broad distribution" (distribución amplia/pública) -- si el contenido queda dentro de un workspace/equipo privado entre miembros invitados, NO cuenta como UGC para esta pregunta. Pero un link público (ej. "compartir doc fuera del equipo") sí puede contar. Para una app B2B típica todo va en "No".',
|
|
47
51
|
},
|
|
48
52
|
{
|
|
49
|
-
key: 'age_content',
|
|
53
|
+
key: 'age_content', section: 'App Information',
|
|
50
54
|
title: 'Age Ratings — Pasos 2-6 (descriptores de contenido)',
|
|
51
|
-
body: 'Mature Themes, Medical/Wellness, Sexuality/Nudity, Violence, Chance-Based Activities -- para una app sin ese tipo de contenido, todo en "None"/"No". El resultado esperado es clasificación 4+.',
|
|
55
|
+
body: 'Mature Themes, Medical/Wellness, Sexuality/Nudity, Violence, Chance-Based Activities -- para una app sin ese tipo de contenido, todo en "None"/"No". El resultado esperado es clasificación 4+. Si marcas "frequent" en "Medical or Treatment Information", Apple te va a pedir además la declaración de dispositivo médico regulado (Regulations & Permits).',
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
|
-
key: 'age_override',
|
|
55
|
-
title: 'Age Ratings — Paso 7 (Age
|
|
56
|
-
body: 'Deja "Not Applicable" salvo que la app esté realmente dirigida a niños (Kids Category) o tenga una razón real para pedir una clasificación más alta.',
|
|
58
|
+
key: 'age_override', section: 'App Information',
|
|
59
|
+
title: 'Age Ratings — Paso 7 (Override) + Age Suitability URL',
|
|
60
|
+
body: 'Deja "Not Applicable" salvo que la app esté realmente dirigida a niños (Kids Category) o tenga una razón real para pedir una clasificación más alta. App Store Connect ya muestra clasificaciones por región (ej. Brasil, Corea, Vietnam) calculadas de tus respuestas -- revísalas, no se llenan aparte. "Age Suitability URL" es opcional: solo si tienes una página propia que explique para qué edades es la app.',
|
|
57
61
|
},
|
|
62
|
+
|
|
63
|
+
// ── Ficha de la versión (Product Page Information) ──
|
|
58
64
|
{
|
|
59
|
-
key: '
|
|
60
|
-
title: '
|
|
61
|
-
body: '
|
|
65
|
+
key: 'promotional_text', section: 'Ficha de la versión',
|
|
66
|
+
title: 'Promotional Text (máx. 170 caracteres)',
|
|
67
|
+
body: 'Aparece arriba de la descripción y se puede cambiar en cualquier momento SIN mandar una versión nueva a revisión -- úsalo para lo que cambia (novedad, temporada, campaña). No cuenta para búsqueda.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: 'description', section: 'Ficha de la versión',
|
|
71
|
+
title: 'Description (máx. 4000 caracteres)',
|
|
72
|
+
body: 'Beneficios primero (1-2 párrafos para quién es y qué resuelve), después una lista real de funciones -- sin inventar lo que la app no hace ni prometer lo que está en otra plataforma. Sin precios ni nombres de planes si lo pagado no se vende como In-App Purchase dentro de la app (Guideline 3.1.1/3.1.3: eso cuenta como "call to action" a una compra externa). No cuenta para búsqueda; editarla exige versión nueva.',
|
|
62
73
|
},
|
|
63
74
|
{
|
|
64
|
-
key: '
|
|
65
|
-
title: '
|
|
66
|
-
body: '
|
|
75
|
+
key: 'keywords', section: 'Ficha de la versión',
|
|
76
|
+
title: 'Keywords (máx. 100 caracteres)',
|
|
77
|
+
body: 'Separadas por coma SIN espacios ("tareas,equipo,kanban"), en singular, sin repetir palabras del nombre/subtítulo ni de la categoría, sin marcas ajenas. Es el único campo de texto que es 100% para búsqueda: aprovecha los 100 caracteres. Las Custom Product Pages pueden reusar estas keywords para aparecer en búsqueda con su propia página.',
|
|
67
78
|
},
|
|
68
79
|
{
|
|
69
|
-
key: '
|
|
70
|
-
title: '
|
|
71
|
-
body: '
|
|
80
|
+
key: 'urls', section: 'Ficha de la versión',
|
|
81
|
+
title: 'Support URL / Marketing URL',
|
|
82
|
+
body: 'Support URL es obligatoria y tiene que responder 200 con una forma real de contacto (página, formulario o correo visible). Marketing URL es opcional (la landing). Pruébalas en el navegador antes de pegarlas -- una SPA sin rewrite puede dar 404 en una ruta profunda.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'copyright', section: 'Ficha de la versión',
|
|
86
|
+
title: 'Copyright',
|
|
87
|
+
body: 'Año + titular de los derechos: la persona o empresa que publica (ej. "2026 Mi Empresa S.A. de C.V."). No lleva el símbolo ©, App Store lo agrega.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: 'whats_new', section: 'Ficha de la versión',
|
|
91
|
+
title: "What's New (solo en actualizaciones)",
|
|
92
|
+
body: 'No aparece en la 1.0. En cada actualización: 2-4 líneas de lo que el usuario nota (no la lista de commits). El changelog de la distribución en Orquestra es la fuente natural.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'screenshots', section: 'Ficha de la versión',
|
|
96
|
+
title: 'Capturas y previews (por dispositivo)',
|
|
97
|
+
body: 'Hasta 10 capturas y 3 previews por tamaño. Hoy pide: iPhone con Dynamic Island (6.1"/6.3": 1179×2556 o 1206×2622; se escalan a los tamaños menores), iPad 13" (2048×2732 o 2064×2752) si la app corre en iPad, y el nuevo iPhone Duo (1398×2034 o 2007×2853) -- obligatorio desde abril 2027 para apps compiladas con el SDK de iOS 27.1 o superior. Las capturas deben mostrar la app en uso (Guideline 2.3.3), sin marcos engañosos. Se suben a la Asset Library y de ahí se asignan a la versión. Si App Store Connect rechaza una medida, guíate por su mensaje, no por esta lista.',
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ── App Review ──
|
|
101
|
+
{
|
|
102
|
+
key: 'review_info', section: 'App Review',
|
|
103
|
+
title: 'App Review Information (sign-in, contacto, notas)',
|
|
104
|
+
body: 'Si la app pide login: usuario y contraseña de una cuenta demo dedicada, ya poblada con datos de ejemplo para que el revisor vea todo sin crear nada. Contacto: nombre, teléfono con código de país y correo de quien responde a Apple. Notes: cómo navegar lo que no es obvio y, si hay funciones que dependen de algo externo (hardware, cuenta de empresa, compras en la web), explicarlo sin precios. Anota aquí DÓNDE vive la contraseña (gestor de contraseñas), no la contraseña en sí.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: 'release_option', section: 'App Review',
|
|
108
|
+
title: 'App Store Version Release',
|
|
109
|
+
body: 'Manual (tú presionas publicar después de la aprobación), automático apenas se aprueba, o automático no antes de una fecha. Para una 1.0 con campaña de lanzamiento, manual o con fecha evita que salga antes de que estén listos la landing y el contenido.',
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// ── Trust & Safety ──
|
|
113
|
+
{
|
|
114
|
+
key: 'privacy_choices_url', section: 'Trust & Safety',
|
|
115
|
+
title: 'Privacy Policy URL + User Privacy Choices URL (opcional)',
|
|
116
|
+
body: 'Privacy Policy URL es obligatoria (pública, responde 200). Junto a ella, App Store Connect deja un campo opcional de "elecciones de privacidad" (ej. opt-out de venta/uso de datos con fines publicitarios, al estilo CCPA). Solo aplica si la app realmente vende/comparte datos con terceros para publicidad -- para una app sin SDKs de publicidad ni venta de datos, se deja en blanco.',
|
|
72
117
|
},
|
|
73
118
|
{
|
|
74
|
-
key: 'data_collection',
|
|
119
|
+
key: 'data_collection', section: 'Trust & Safety',
|
|
75
120
|
title: 'Data Collection (App Privacy)',
|
|
76
|
-
body: '
|
|
121
|
+
body: 'Por cada tipo de dato que la app recolecta: propósito (App Functionality / Analytics / Advertising / etc.), si está vinculado a la identidad y si se usa para "tracking" (definición estricta de Apple: compartir con OTRAS empresas para publicidad/medición cross-app, no cualquier analítica interna). Para una app con Firebase Auth + Firestore + Analytics + Crashlytics, sin ads: Name / Email / User ID / User Content (Contact Info, Identifiers, User Content) con App Functionality; Crash Data (Diagnostics) si hay Crashlytics -- se olvida seguido; Product Interaction (Usage Data) con Analytics; Device ID según su uso real (push/sesión = App Functionality, solo medición = Analytics). Vinculados a identidad SOLO si el código llama a Analytics.setUserID/Crashlytics.setUserID con el uid real. Nada es tracking si no hay SDKs de publicidad/ATT -- ojo con dependencias transitivas (ej. conversiones de Google Ads dentro de GoogleAppMeasurement) aunque el código propio no las llame.',
|
|
77
122
|
},
|
|
78
123
|
{
|
|
79
|
-
key: '
|
|
124
|
+
key: 'accessibility', section: 'Trust & Safety',
|
|
125
|
+
title: 'App Accessibility (Accessibility Nutrition Labels)',
|
|
126
|
+
body: 'Sección nueva: declaras qué funciones de accesibilidad soporta la app en sus tareas comunes, y se muestran en la página de producto. Son 9: VoiceOver, Voice Control, Larger Text (Dynamic Type hasta 200%), Dark Interface, Differentiate Without Color Alone, Sufficient Contrast, Reduced Motion, Captions y Audio Descriptions (estas dos solo si hay video/audio). Declara solo lo que funciona de verdad en TODAS las tareas comunes (Apple publica criterios por función) -- revísalo en el código (ej. fuentes con Dynamic Type, colores con variante oscura, animaciones que respetan Reduce Motion) y en un iPhone físico con cada ajuste prendido. Es opcional, pero vacío se ve como "el desarrollador no ha indicado".',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: 'delete_account', section: 'Trust & Safety',
|
|
80
130
|
title: 'Eliminar cuenta desde la app (Guideline 5.1.1(v))',
|
|
81
131
|
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.',
|
|
82
132
|
},
|
|
83
133
|
{
|
|
84
|
-
key: 'sign_in_apple',
|
|
134
|
+
key: 'sign_in_apple', section: 'Trust & Safety',
|
|
85
135
|
title: 'Sign in with Apple si hay login de terceros (Guideline 4.8)',
|
|
86
136
|
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.',
|
|
87
137
|
},
|
|
138
|
+
|
|
139
|
+
// ── Cumplimiento ──
|
|
140
|
+
{
|
|
141
|
+
key: 'encryption', section: 'Cumplimiento',
|
|
142
|
+
title: 'App Encryption (export compliance)',
|
|
143
|
+
body: 'Agrega `ITSAppUsesNonExemptEncryption` al Info.plist: `NO` si la app solo usa el cifrado del sistema (HTTPS/TLS, Keychain, CryptoKit para lo estándar) -- es el caso de casi todas las apps. Sin la clave, App Store Connect pregunta en CADA build subido y el build queda trabado en "Missing Compliance" hasta que contestes. `YES` solo con cifrado propio o no estándar, y ahí Apple pide documentación.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: 'regulations', section: 'Cumplimiento',
|
|
147
|
+
title: 'App Store Regulations & Permits (DSA y otros)',
|
|
148
|
+
body: 'Digital Services Act (UE): declarar si eres "trader" (comerciante). Si la app genera ingresos o la publica una empresa, casi seguro eres trader: Apple pide dirección, teléfono y correo verificados y los MUESTRA en la página de producto en la UE. Como "non-trader" Apple puede limitar la distribución en la UE si su revisión no coincide. Los demás (ICP de China continental, licencia de juegos de Vietnam, dispositivo médico regulado) solo aplican si distribuyes en esos mercados o eres de esas categorías.',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: 'payments_iap', section: 'Cumplimiento',
|
|
152
|
+
title: 'Pagos dentro de la app (Guideline 3.1.1)',
|
|
153
|
+
body: 'Si algo pagado DESBLOQUEA funcionalidad dentro de la app (planes, créditos, funciones premium), Apple exige venderlo como In-App Purchase en iOS -- aunque se compre en la web y la app no tenga botones de compra (rechazo real de Orquestra por 3.1.1). La excepción 3.1.3(f) solo protege apps gratis cuyo contenido pago se usa FUERA de la app. Para ofrecer IAP: Paid Applications Agreement aceptado por el Account Holder, datos fiscales (en México, RFC/constancia de situación fiscal) y cuenta bancaria en Business → Agreements, y los productos en In-App Purchases/Subscriptions. Sin pagos, anótalo aquí como "No aplica".',
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// ── Pricing and Availability ──
|
|
157
|
+
{
|
|
158
|
+
key: 'pricing', section: 'Pricing and Availability',
|
|
159
|
+
title: 'App Pricing',
|
|
160
|
+
body: 'Depende del modelo de negocio real del producto -- no asumas que es gratis, esto lo decide cada proyecto. El país base define cómo Apple ajusta el precio en las demás regiones. Tax Category: "App Store software" salvo un caso especial.',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: 'availability', section: 'Pricing and Availability',
|
|
164
|
+
title: 'App Availability (países, Mac y Vision Pro)',
|
|
165
|
+
body: '"All Countries or Regions" es el default razonable salvo que haya una restricción regulatoria real específica de la app. Las apps de iPhone/iPad quedan disponibles por default en Macs con Apple Silicon y en Apple Vision Pro -- apágalo si la app no se probó ahí o depende de hardware del iPhone.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: 'distribution_method', section: 'Pricing and Availability',
|
|
169
|
+
title: 'App Distribution Methods (público o privado)',
|
|
170
|
+
body: 'Público (cualquiera la encuentra en App Store, el default) o Privado (custom app solo para organizaciones en Apple Business/School Manager). NO se puede cambiar después de la aprobación -- decídelo antes de la primera revisión. "Reduced price on Apple School Manager" solo importa si la app es de pago.',
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// ── Crecimiento (después de la aprobación) ──
|
|
174
|
+
{
|
|
175
|
+
key: 'creative_assets', section: 'Crecimiento (después de la aprobación)',
|
|
176
|
+
title: 'Header and Search Results + Asset Library',
|
|
177
|
+
body: 'Nuevo desde oct 2026: piezas creativas (imagen o video) que se suman a las capturas -- Header = banner arriba de la página de producto (21:9, 3840×1646) y Search Results = lo que se ve en los resultados de búsqueda (3:2, de 1920×1280 a 3840×2560); una pieza universal 16:9 de 5244×2950 sirve para las dos. Solo se ven en iOS/iPadOS 27 o superior (en versiones anteriores siguen las capturas). Son piezas de diseño, no capturas de la app. Se suben a la Asset Library (repositorio de capturas, previews y piezas creativas independiente de las versiones), se mandan a revisión ("Add for Review") sin versión nueva y luego se reusan en la versión, en Custom Product Pages, en pruebas de PPO o en In-App Events.',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
key: 'custom_product_pages', section: 'Crecimiento (después de la aprobación)',
|
|
181
|
+
title: 'Custom Product Pages (hasta 70)',
|
|
182
|
+
body: 'Variantes de tu página de producto, cada una con sus capturas, header y promotional text, un link propio (`?ppid=`) para campañas/anuncios, keywords asignadas (de las de la última versión aprobada) para aparecer en búsqueda orgánica en vez de la página default, y un deep link para que "Abrir" lleve a una pantalla específica. Una por público o caso de uso real (ej. "freelancers" vs "equipos chicos"), no una por anuncio. Necesitan una versión ya aprobada.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: 'product_page_optimization', section: 'Crecimiento (después de la aprobación)',
|
|
186
|
+
title: 'Product Page Optimization (pruebas A/B)',
|
|
187
|
+
body: 'Prueba hasta 3 variantes de la página default (ícono, capturas, previews y piezas creativas) contra la original, mostradas al azar a usuarios elegibles y medidas en App Analytics. Solo tiene sentido con tráfico real (cientos de visitas por semana) -- anota aquí la hipótesis antes de crear la prueba (qué cambias y qué esperas que mejore).',
|
|
188
|
+
},
|
|
88
189
|
],
|
|
89
190
|
|
|
191
|
+
// `optional: true` = no viene seleccionada por default al aplicar (pasos
|
|
192
|
+
// de crecimiento que solo tienen sentido con la app ya aprobada).
|
|
90
193
|
tasks: [
|
|
91
194
|
{
|
|
92
195
|
title: 'Verificar/crear política de privacidad pública (URL)',
|
|
@@ -95,43 +198,88 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
95
198
|
},
|
|
96
199
|
{
|
|
97
200
|
title: 'Verificar/crear página o contacto de soporte',
|
|
98
|
-
description: 'Apple pide una URL de soporte visible para el usuario (puede ser un mailto, un formulario o una página de FAQ).',
|
|
201
|
+
description: 'Apple pide una URL de soporte visible para el usuario (puede ser un mailto, un formulario o una página de FAQ). Probarla en el navegador: tiene que responder 200.',
|
|
99
202
|
type: 'business', priority: 'high', tags: ['publicacion', 'ios'], offsetDays: 2, checkKey: 'support',
|
|
100
203
|
},
|
|
101
204
|
{
|
|
102
|
-
title: 'Confirmar
|
|
103
|
-
description: '
|
|
104
|
-
type: '
|
|
205
|
+
title: 'Confirmar que la app permite eliminar la cuenta desde adentro',
|
|
206
|
+
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.',
|
|
207
|
+
type: 'security', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 1,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
title: 'Verificar que si hay login de terceros (Google/Facebook/etc.), también esté "Sign in with Apple"',
|
|
211
|
+
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.',
|
|
212
|
+
type: 'security', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 1,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
title: 'Revisar si algo pagado desbloquea funciones en la app (In-App Purchase, Guideline 3.1.1)',
|
|
216
|
+
description: 'Si planes, créditos o funciones premium cambian lo que se puede hacer DENTRO de la app iOS, Apple exige In-App Purchase aunque se compre en la web. Decidir antes de mandar a revisión: (a) ofrecer IAP -- requiere Paid Applications Agreement, datos fiscales (RFC/constancia de situación fiscal en México) y cuenta bancaria en Business → Agreements --, o (b) que la versión iOS no dependa de nada pagado. Quitar de la app y de la ficha precios, nombres de planes y cualquier invitación a comprar afuera.',
|
|
217
|
+
type: 'business', priority: 'high', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 3,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
title: 'Agregar ITSAppUsesNonExemptEncryption al Info.plist',
|
|
221
|
+
description: 'Export compliance: `NO` si la app solo usa el cifrado del sistema (HTTPS/TLS, Keychain). Sin la clave, cada build subido queda en "Missing Compliance" hasta contestar a mano en App Store Connect.',
|
|
222
|
+
type: 'tech', priority: 'medium', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 3,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
title: 'Declarar el estatus de trader de la Digital Services Act (UE)',
|
|
226
|
+
description: 'App Information → App Store Regulations & Permits. Si la app genera ingresos o la publica una empresa, declararse trader con dirección, teléfono y correo verificados (Apple los muestra en la UE). Decidirlo con la persona dueña de la cuenta, no por default.',
|
|
227
|
+
type: 'business', priority: 'medium', tags: ['publicacion', 'ios', 'compliance'], offsetDays: 3,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
title: 'Confirmar pricing, disponibilidad por país y método de distribución',
|
|
231
|
+
description: 'Definir el modelo de precio real (gratis, de pago, suscripción) y el alcance de países -- "All Countries or Regions" es el default razonable salvo restricción regulatoria específica. Revisar disponibilidad en Mac con Apple Silicon y Vision Pro. Elegir público o privado: el método de distribución NO se puede cambiar después de la aprobación.',
|
|
232
|
+
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 3,
|
|
105
233
|
},
|
|
106
234
|
{
|
|
107
235
|
title: 'Preparar cuenta demo para el reviewer, si el login lo requiere',
|
|
108
|
-
description: 'Si la app pide login, "App Review Information" necesita un usuario y contraseña de
|
|
236
|
+
description: 'Si la app pide login, "App Review Information" necesita un usuario y contraseña de una cuenta dedicada, ya poblada con datos de ejemplo, más contacto y notas para el revisor. La contraseña va en App Store Connect y en un gestor de contraseñas, no en la descripción de esta tarea.',
|
|
109
237
|
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 5,
|
|
110
238
|
},
|
|
239
|
+
{
|
|
240
|
+
title: 'Escribir la ficha de la versión: promotional text, description, keywords, URLs y copyright',
|
|
241
|
+
description: 'Promotional text (170, editable sin revisión), description (4000, beneficios primero y funciones reales, sin precios si no hay IAP), keywords (100, separadas por coma sin espacios, sin repetir nombre/subtítulo), Support URL, Marketing URL y copyright. Respaldo en el formulario de la Guía de publicación.',
|
|
242
|
+
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 6,
|
|
243
|
+
},
|
|
111
244
|
{
|
|
112
245
|
title: 'Preparar capturas de pantalla en los tamaños exactos que pide App Store Connect',
|
|
113
|
-
description: 'Las dimensiones válidas cambian con cada generación
|
|
246
|
+
description: 'iPhone con Dynamic Island (1179×2556 o 1206×2622), iPad 13" si la app corre en iPad (2048×2732 o 2064×2752) e iPhone Duo (1398×2034 o 2007×2853, obligatorio desde abril 2027 con el SDK de iOS 27.1+). Las dimensiones válidas cambian con cada generación -- si App Store Connect rechaza una captura al subirla, el mensaje de error indica el tamaño exacto que espera para ese dispositivo; ajusta según ese mensaje real, no de memoria.',
|
|
114
247
|
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 7, checklistTool: 'screenshots',
|
|
115
248
|
},
|
|
116
249
|
{
|
|
117
250
|
title: 'Completar el cuestionario de privacidad de la tienda (Data Collection)',
|
|
118
|
-
description: 'Por cada tipo de dato que la app recolecta (nombre, email, user ID, contenido del usuario, device ID, product interaction), declarar el propósito (App Functionality / Analytics / etc.), si está vinculado a la identidad y si se usa para tracking (definición estricta de Apple: compartir con otras empresas para publicidad/medición cross-app).',
|
|
251
|
+
description: 'Por cada tipo de dato que la app recolecta (nombre, email, user ID, contenido del usuario, crash data, device ID, product interaction), declarar el propósito (App Functionality / Analytics / etc.), si está vinculado a la identidad y si se usa para tracking (definición estricta de Apple: compartir con otras empresas para publicidad/medición cross-app). Revisar en el código si se llama a setUserID de Analytics/Crashlytics.',
|
|
119
252
|
type: 'business', priority: 'high', tags: ['publicacion', 'ios'], offsetDays: 9,
|
|
120
253
|
},
|
|
254
|
+
{
|
|
255
|
+
title: 'Declarar App Accessibility según lo que la app soporta de verdad',
|
|
256
|
+
description: 'App Store → App Accessibility: marcar solo las funciones (VoiceOver, Voice Control, Larger Text, Dark Interface, Differentiate Without Color, Sufficient Contrast, Reduced Motion, Captions, Audio Descriptions) que funcionan en todas las tareas comunes. Probar cada una en un iPhone físico con el ajuste prendido antes de declararla.',
|
|
257
|
+
type: 'business', priority: 'medium', tags: ['publicacion', 'ios', 'accesibilidad'], offsetDays: 9,
|
|
258
|
+
},
|
|
121
259
|
{
|
|
122
260
|
title: 'Completar clasificación de edad / Age Rating',
|
|
123
|
-
description: 'Recorrer los 7 pasos del cuestionario de Age Ratings en App Store Connect: Features, los 5 descriptores de contenido y Age Categories/Override. Para una app B2B sin ese contenido, el resultado esperado es 4+.',
|
|
261
|
+
description: 'Recorrer los 7 pasos del cuestionario de Age Ratings en App Store Connect: Features, los 5 descriptores de contenido y Age Categories/Override. Para una app B2B sin ese contenido, el resultado esperado es 4+. Revisar las clasificaciones regionales que calcula App Store Connect.',
|
|
124
262
|
type: 'business', priority: 'medium', tags: ['publicacion', 'ios'], offsetDays: 10,
|
|
125
263
|
},
|
|
126
264
|
{
|
|
127
|
-
title: '
|
|
128
|
-
description: '
|
|
129
|
-
type: '
|
|
265
|
+
title: 'Elegir cómo se publica la versión al aprobarse (manual, automático o con fecha)',
|
|
266
|
+
description: 'App Store Version Release. Para una 1.0 con campaña de lanzamiento, manual o con fecha evita que salga antes de que estén listos la landing y el contenido.',
|
|
267
|
+
type: 'business', priority: 'low', tags: ['publicacion', 'ios'], offsetDays: 10,
|
|
130
268
|
},
|
|
131
269
|
{
|
|
132
|
-
title: '
|
|
133
|
-
description: '
|
|
134
|
-
type: '
|
|
270
|
+
title: 'Preparar las piezas creativas: header y resultados de búsqueda (iOS 27+)',
|
|
271
|
+
description: 'Después de la aprobación. Header 21:9 (3840×1646), Search Results 3:2 (1920×1280 a 3840×2560) o una universal 16:9 (5244×2950). Son piezas de diseño, no capturas. Subirlas a la Asset Library y mandarlas a revisión ("Add for Review"), sin versión nueva.',
|
|
272
|
+
type: 'design', priority: 'low', tags: ['publicacion', 'ios', 'crecimiento'], offsetDays: 21, optional: true,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
title: 'Crear Custom Product Pages por público con sus keywords y deep link',
|
|
276
|
+
description: 'Después de la aprobación. Una página por público o caso de uso real (no una por anuncio), con capturas/header propios, keywords tomadas de la versión aprobada y un deep link a la pantalla relevante. Usar su link `?ppid=` en las campañas de Contenido.',
|
|
277
|
+
type: 'business', priority: 'low', tags: ['publicacion', 'ios', 'crecimiento'], offsetDays: 28, optional: true,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
title: 'Planear una prueba de Product Page Optimization',
|
|
281
|
+
description: 'Cuando haya tráfico real. Hasta 3 variantes de la página default (ícono, capturas o piezas creativas), con la hipótesis escrita antes de crearla y revisión de resultados en App Analytics.',
|
|
282
|
+
type: 'business', priority: 'low', tags: ['publicacion', 'ios', 'crecimiento'], offsetDays: 45, optional: true,
|
|
135
283
|
},
|
|
136
284
|
],
|
|
137
285
|
},
|
|
@@ -378,7 +526,7 @@ export const PLATFORM_PLAYBOOKS = {
|
|
|
378
526
|
{
|
|
379
527
|
title: 'Medir y corregir Core Web Vitals en PageSpeed Insights (móvil)',
|
|
380
528
|
description: 'Correr PageSpeed Insights sobre la home y las páginas clave. Objetivo: LCP < 2.5s, CLS < 0.1, INP < 200ms. Atacar primero imágenes sin comprimir, fuentes bloqueantes y JS sin dividir.',
|
|
381
|
-
type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5,
|
|
529
|
+
type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5, checkKey: 'core_web_vitals',
|
|
382
530
|
},
|
|
383
531
|
{
|
|
384
532
|
title: 'Fijar dominio canónico y redirecciones 301 (www / http → https)',
|
package/src/server.mjs
CHANGED
|
@@ -110,7 +110,7 @@ Si get_project_context devuelve un proyecto vacío (sin módulos ni tareas), es
|
|
|
110
110
|
|
|
111
111
|
Al aplicar un playbook con apply_stage_playbook -- sobre todo el de etapa "post" (marketing/contenido) -- nunca asumas herramientas o canales que este proyecto no tiene registrados en Orquestra. Antes de crear tareas de contenido/publicación, llama a list_channels: si viene vacío, la plantilla ya incluye una tarea para conectar canales reales primero -- no la saltees ni generes tareas del tipo "publica en Instagram/LinkedIn/TikTok" para un canal que el proyecto no tiene. Si list_channels SÍ trae canales reales, no te quedes solo con la tarea recordatorio de "programa tu contenido" -- además creá piezas REALES con create_content (un plan de lanzamiento concreto: teaser, anuncio, demo del caso de uso, historia de por qué se construyó, 1-2 testimonios, repartidos entre esos canales) usando siempre un channelId de list_channels, nunca un canal inventado. Mismo criterio para cualquier otra tarea del playbook que dependa de una integración -- si el hueco es real (ej. no hay servicios/distribuciones configurados), la tarea correcta es configurar eso primero, no asumir que ya existe.
|
|
112
112
|
|
|
113
|
-
Al aplicar apply_platform_playbook (Platform Playbook de publicación iOS/Android) con publishForm: solo completa las claves para las que tengas base real del proyecto (código, get_project_context, servicios/entornos ya configurados) -- mismo criterio que el botón "Completar con IA" de la web: category/content_rights/data_collection/content_rating suelen inferirse bien para una app B2B sin ads/tracking, pero pricing, availability, package/bundle ID, cuentas demo para el reviewer o el nombre exacto de la app NUNCA se inventan -- dejalas fuera de publishForm para que el usuario las complete a mano. Si la tool devuelve publishFormConflict, la distribución ya tenía datos guardados en esas claves y no se escribió nada -- mostrale al usuario el valor actual vs. el propuesto de cada una, y repetí el llamado con confirmReplace:true solo si confirma explícito el reemplazo (no lo asumas ni lo repitas sin esa confirmación).
|
|
113
|
+
Al aplicar apply_platform_playbook (Platform Playbook de publicación iOS/Android) con publishForm: solo completa las claves para las que tengas base real del proyecto (código, get_project_context, servicios/entornos ya configurados) -- mismo criterio que el botón "Completar con IA" de la web: category/content_rights/data_collection/content_rating suelen inferirse bien para una app B2B sin ads/tracking (y en iOS también promotional_text/description/keywords/custom_product_pages a partir de la descripción real del proyecto, y accessibility/encryption SOLO si revisaste el código de la app), pero pricing, availability, distribution_method, release_option, regulations (estatus de trader de la DSA), payments_iap, package/bundle ID, cuentas demo para el reviewer (review_info: nunca escribas una contraseña ahí) o el nombre exacto de la app NUNCA se inventan -- dejalas fuera de publishForm para que el usuario las complete a mano. Si la tool devuelve publishFormConflict, la distribución ya tenía datos guardados en esas claves y no se escribió nada -- mostrale al usuario el valor actual vs. el propuesto de cada una, y repetí el llamado con confirmReplace:true solo si confirma explícito el reemplazo (no lo asumas ni lo repitas sin esa confirmación).
|
|
114
114
|
|
|
115
115
|
Analítica y anuncios en un sitio web: antes de instalar o tocar Google Analytics 4, Meta Pixel o Tag Manager en el código, llama a get_tracking_ids -- si la distribución web ya tiene el ID guardado (ga4_measurement_id, meta_pixel_id, gtm_container_id), úsalo tal cual; nunca inventes un ID ni se lo vuelvas a pedir al usuario. Si falta y el usuario te lo da (o lo lees literal en el código/consola real), guárdalo con set_tracking_ids para que el resto de agentes y la guía web lo vean. Si ya hay uno distinto guardado, no lo reemplaces sin confirmación explícita (confirmReplace).
|
|
116
116
|
|
|
@@ -337,6 +337,26 @@ export function createServer() {
|
|
|
337
337
|
{}
|
|
338
338
|
)
|
|
339
339
|
|
|
340
|
+
// PageSpeed Insights por distribución web (ORQ-377). La última medición
|
|
341
|
+
// también viene en list_distributions (campo `pagespeed` de cada una).
|
|
342
|
+
registerRead(
|
|
343
|
+
'get_pagespeed',
|
|
344
|
+
'/v1/pagespeed',
|
|
345
|
+
'Historial de mediciones de PageSpeed Insights (móvil) de una distribución web, más reciente primero: scores de Performance/Accesibilidad/Buenas prácticas/SEO, métricas de laboratorio (LCP, CLS, TBT), Core Web Vitals de usuarios reales (CrUX: LCP, INP, CLS con categoría FAST/AVERAGE/SLOW; null si el sitio no tiene tráfico suficiente), oportunidades con ahorro estimado y auditorías que fallan. Se mide solo cada lunes; para una medición nueva usa measure_pagespeed.',
|
|
346
|
+
{
|
|
347
|
+
distributionId: z.string().describe('Distribución type="web" con domain configurado (ver list_distributions)'),
|
|
348
|
+
limit: z.number().int().min(1).max(100).optional().describe('Cuántas mediciones traer (default 20)'),
|
|
349
|
+
},
|
|
350
|
+
(args, projectId) => ({ projectId, distributionId: args.distributionId, limit: args.limit })
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
registerCreate(
|
|
354
|
+
'measure_pagespeed',
|
|
355
|
+
'/v1/pagespeed',
|
|
356
|
+
'Pide una medición nueva de PageSpeed Insights (móvil) para una distribución web con dominio. Corre en segundo plano (10-40 s): responde de inmediato con status "running"; el resultado aparece después en el campo `pagespeed` de la distribución (list_distributions) y en get_pagespeed. 409 si ya hay una en curso.',
|
|
357
|
+
{ distributionId: z.string().describe('Distribución type="web" con domain configurado') }
|
|
358
|
+
)
|
|
359
|
+
|
|
340
360
|
registerRead(
|
|
341
361
|
'get_infra_status',
|
|
342
362
|
'/v1/infra',
|
|
@@ -512,7 +532,7 @@ export function createServer() {
|
|
|
512
532
|
server.registerTool(
|
|
513
533
|
'get_platform_playbook',
|
|
514
534
|
{
|
|
515
|
-
description: 'Guía de referencia + checklist accionable para publicar en App Store Connect (iOS) o Google Play Console (Android) -- "Platform Playbook", paralelo a get_stage_playbook pero sobre una PLATAFORMA/distribución concreta, no la etapa del proyecto. No llama a la API -- datos estáticos. "referenceGuide" es texto de referencia (no genera tareas); "tasks" es el checklist accionable (misma forma que get_stage_playbook, se puede convertir en módulo + tareas reales).',
|
|
535
|
+
description: 'Guía de referencia + checklist accionable para publicar en App Store Connect (iOS) o Google Play Console (Android) -- "Platform Playbook", paralelo a get_stage_playbook pero sobre una PLATAFORMA/distribución concreta, no la etapa del proyecto. No llama a la API -- datos estáticos. "referenceGuide" es texto de referencia (no genera tareas); "tasks" es el checklist accionable (misma forma que get_stage_playbook, se puede convertir en módulo + tareas reales). En iOS cada ítem de referenceGuide trae "section" (mismo orden que el menú de App Store Connect) y las tareas con optional:true son de crecimiento después de la aprobación (apply_platform_playbook no las crea salvo includeOptional).',
|
|
516
536
|
inputSchema: { platform: z.enum(['ios', 'android', 'web']).describe('ios (App Store Connect) | android (Google Play Console) | web (SEO y analítica: Search Console, GA4/Firebase Analytics, sitemap, meta tags, Core Web Vitals)') },
|
|
517
537
|
},
|
|
518
538
|
async ({ platform }) => textResult(PLATFORM_PLAYBOOKS[platform])
|
|
@@ -740,7 +760,9 @@ export function createServer() {
|
|
|
740
760
|
const ACTIVITY_FIELDS_SCHEMA = {
|
|
741
761
|
location: z.string().optional().describe('Plataforma o lugar ("Zoom", "Oficina del cliente") o contexto libre para un seguimiento.'),
|
|
742
762
|
participants: z.array(z.string()).optional().describe('Nombres libres de quienes participan (cliente, equipo) -- no uids.'),
|
|
743
|
-
description: z.string().optional().describe('
|
|
763
|
+
description: z.string().optional().describe('Notas previas.'),
|
|
764
|
+
agendaItems: z.array(z.union([z.string(), z.object({ id: z.string().optional(), text: z.string() })])).optional()
|
|
765
|
+
.describe('Puntos a revisar en la reunión (ej. "Revisar [[ORQ-123]]"). Lista simple, NO checklist. Reemplaza la lista COMPLETA: para agregar uno, lee la actual con list_activities y manda todos (con sus id para conservarlos).'),
|
|
744
766
|
outcome: z.string().optional().describe('Resultado / minuta corta -- qué se acordó. Llénalo al marcarla hecha.'),
|
|
745
767
|
nextStep: z.string().optional().describe('Siguiente paso acordado. Si es trabajo real, además crea la tarea con create_task y liga su id en nextStepTaskId.'),
|
|
746
768
|
nextStepTaskId: z.string().optional().describe('id de la tarea creada desde nextStep.'),
|
|
@@ -2635,12 +2657,12 @@ export function createServer() {
|
|
|
2635
2657
|
|
|
2636
2658
|
registerRead('list_agents', '/v1/agents',
|
|
2637
2659
|
'Lista los agentes (kind+máquina+stack+ruta local) configurados en un proyecto. Úsala antes de create_agent o de create_agent_run con stack, para saber si ya existe uno.',
|
|
2638
|
-
{ kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
|
|
2660
|
+
{ kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse: performance/accesibilidad/buenas prácticas/SEO + LCP/CLS/TBT) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
|
|
2639
2661
|
(args, projectId) => ({ projectId, ...(args.kind ? { kind: args.kind } : {}) }))
|
|
2640
2662
|
|
|
2641
2663
|
registerCreate('create_agent', '/v1/agents', 'Crea un agente: asocia un kind ("qa" | "security" | "performance" | "screenshots") a una máquina (runner, ver Workspace → Máquinas), un stack de este proyecto y una ruta de checkout en ESA máquina. kind="performance" además necesita perfUrl.', {
|
|
2642
2664
|
name: z.string().describe('Nombre a mostrar, ej. "Agente web"'),
|
|
2643
|
-
kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
|
|
2665
|
+
kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse: performance/accesibilidad/buenas prácticas/SEO + LCP/CLS/TBT) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
|
|
2644
2666
|
runnerId: z.string().describe('ID de la máquina emparejada -- ver list_agents o Workspace → Máquinas'),
|
|
2645
2667
|
stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los 4 kinds. security/performance solo tienen lógica real para "web" hoy; screenshots solo para "ios"/"android"'),
|
|
2646
2668
|
localPath: z.string().optional().describe('Requerido para los 4 kinds -- ruta absoluta del checkout en esa máquina'),
|
|
@@ -2655,7 +2677,14 @@ export function createServer() {
|
|
|
2655
2677
|
plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- recordatorio manual de "próxima corrida planeada". Ningún kind tiene scheduling real (todo trigger es manual/push): esto NO dispara ninguna corrida sola, solo alimenta el calendario global.'),
|
|
2656
2678
|
perfUrl: z.string().optional().describe('Requerido si kind="performance" -- URL viva (ej. staging/prod) contra la que corre Lighthouse'),
|
|
2657
2679
|
maxBundleSizeKb: z.number().optional().describe('Específico de kind="performance" -- umbral en KB para el paso de tamaño de build (default 5000)'),
|
|
2658
|
-
minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse (0-100) para pasar (default 50)'),
|
|
2680
|
+
minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse en Performance (0-100) para pasar (default 50)'),
|
|
2681
|
+
perfUrls: z.array(z.string()).optional().describe('Específico de kind="performance" -- URLs ADICIONALES a perfUrl (ej. /precios, /blog): Lighthouse corre un paso por cada una'),
|
|
2682
|
+
minAccessibilityScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Accesibilidad (0-100, default 90)'),
|
|
2683
|
+
minBestPracticesScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Buenas prácticas (0-100, default 90)'),
|
|
2684
|
+
minSeoScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de SEO (0-100, default 90)'),
|
|
2685
|
+
maxLcpMs: z.number().optional().describe('Específico de kind="performance" -- LCP máximo en ms (default 2500, umbral "bueno" de Google)'),
|
|
2686
|
+
maxCls: z.number().optional().describe('Específico de kind="performance" -- CLS máximo (default 0.1)'),
|
|
2687
|
+
maxTbtMs: z.number().optional().describe('Específico de kind="performance" -- TBT máximo en ms (default 200, proxy de laboratorio de INP)'),
|
|
2659
2688
|
})
|
|
2660
2689
|
|
|
2661
2690
|
registerUpdate('update_agent', '/v1/agents', 'Edita un agente existente.', {
|
|
@@ -2676,6 +2705,13 @@ export function createServer() {
|
|
|
2676
2705
|
perfUrl: z.string().optional().describe('Ver create_agent -- específico de kind="performance"'),
|
|
2677
2706
|
maxBundleSizeKb: z.number().optional(),
|
|
2678
2707
|
minPerfScore: z.number().optional(),
|
|
2708
|
+
perfUrls: z.array(z.string()).optional().describe('Ver create_agent -- reemplaza la lista completa, [] la vacía'),
|
|
2709
|
+
minAccessibilityScore: z.number().optional(),
|
|
2710
|
+
minBestPracticesScore: z.number().optional(),
|
|
2711
|
+
minSeoScore: z.number().optional(),
|
|
2712
|
+
maxLcpMs: z.number().optional(),
|
|
2713
|
+
maxCls: z.number().optional(),
|
|
2714
|
+
maxTbtMs: z.number().optional(),
|
|
2679
2715
|
})
|
|
2680
2716
|
|
|
2681
2717
|
registerCreate('create_agent_run', '/v1/agent-runs', 'Dispara una corrida de agente (qa: lint/tests/build/E2E; security: npm audit + gitleaks; performance: build size + Lighthouse; screenshots: captura de pantallas para el checklist de publicación) en la rama indicada. Necesita al menos un agente activo para el stack (o falla explícito -- ver list_agents/create_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_agent_run.', {
|
|
@@ -2820,9 +2856,10 @@ export function createServer() {
|
|
|
2820
2856
|
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.'),
|
|
2821
2857
|
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.'),
|
|
2822
2858
|
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.'),
|
|
2859
|
+
includeOptional: z.boolean().optional().describe('Default false. Las tareas marcadas optional:true en get_platform_playbook (crecimiento después de la aprobación: piezas creativas, Custom Product Pages, Product Page Optimization) no se crean salvo que mandes true -- igual que en la web, donde vienen desmarcadas.'),
|
|
2823
2860
|
},
|
|
2824
2861
|
},
|
|
2825
|
-
async ({ projectId, platform, distributionId, moduleName, publishForm, confirmReplace, force }) => {
|
|
2862
|
+
async ({ projectId, platform, distributionId, moduleName, publishForm, confirmReplace, force, includeOptional }) => {
|
|
2826
2863
|
try {
|
|
2827
2864
|
const pid = resolveProjectId(projectId)
|
|
2828
2865
|
const playbook = PLATFORM_PLAYBOOKS[platform]
|
|
@@ -2846,6 +2883,7 @@ export function createServer() {
|
|
|
2846
2883
|
|
|
2847
2884
|
const taskIds = []
|
|
2848
2885
|
for (const t of playbook.tasks) {
|
|
2886
|
+
if (t.optional && !includeOptional) continue
|
|
2849
2887
|
const dueDate = new Date(Date.now() + t.offsetDays * 86400000).toISOString().slice(0, 10)
|
|
2850
2888
|
const created = await apiPost(config, '/v1/tasks', {
|
|
2851
2889
|
projectId: pid,
|
package/src/setup.mjs
CHANGED
|
@@ -78,22 +78,72 @@ export async function runSetup() {
|
|
|
78
78
|
const agentLabelAnswer = await prompt('¿Cómo se llama este agente/herramienta? [Claude Code]: ')
|
|
79
79
|
const agentLabel = agentLabelAnswer.trim() || 'Claude Code'
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
// askIfMissing: si no hay workspace la API no crea nada solo -- avisa
|
|
82
|
+
// (needsWorkspace) y aquí se le pide el nombre al usuario.
|
|
83
|
+
const callBootstrap = async (extra = {}) => {
|
|
84
|
+
const res = await fetch(`${apiBase}/v1/bootstrap`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ agentLabel, askIfMissing: true, ...extra }),
|
|
88
|
+
})
|
|
89
|
+
const body = await res.json()
|
|
90
|
+
if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
|
|
91
|
+
return body
|
|
92
|
+
}
|
|
93
|
+
const askName = async (question) => {
|
|
94
|
+
for (;;) {
|
|
95
|
+
const name = (await prompt(question)).trim()
|
|
96
|
+
if (name) return name
|
|
97
|
+
console.log('El nombre no puede estar vacío.')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let body = await callBootstrap()
|
|
102
|
+
if (body.needsWorkspaceChoice) {
|
|
103
|
+
// Miembro de varios workspaces: elegir aquí no cambia el workspace
|
|
104
|
+
// actual de la web, solo a cuál queda conectada esta carpeta.
|
|
105
|
+
const { workspaces, currentWorkspaceId } = body
|
|
106
|
+
console.log('\nPerteneces a varios workspaces. ¿En cuál quieres trabajar desde esta carpeta?')
|
|
107
|
+
workspaces.forEach((w, i) => console.log(` ${i + 1}. ${w.name}${w.id === currentWorkspaceId ? ' (el que tienes abierto en la web)' : ''}`))
|
|
108
|
+
const currentIdx = workspaces.findIndex(w => w.id === currentWorkspaceId) + 1
|
|
109
|
+
let idx
|
|
110
|
+
for (;;) {
|
|
111
|
+
const answer = (await prompt(currentIdx ? `Número [Enter para usar "${workspaces[currentIdx - 1].name}"]: ` : 'Número: ')).trim()
|
|
112
|
+
idx = answer === '' && currentIdx ? currentIdx : parseInt(answer, 10)
|
|
113
|
+
if (idx >= 1 && idx <= workspaces.length) break
|
|
114
|
+
console.log('Selección inválida.')
|
|
115
|
+
}
|
|
116
|
+
body = await callBootstrap({ workspaceId: workspaces[idx - 1].id })
|
|
117
|
+
} else if (body.needsWorkspace) {
|
|
118
|
+
console.log('\nTodavía no tienes un workspace en Orquestra. Primero vamos a crearlo')
|
|
119
|
+
console.log('(agrupa todos tus proyectos: tu empresa, tu equipo o tu nombre si trabajas solo).')
|
|
120
|
+
const workspaceName = await askName('Nombre del workspace: ')
|
|
121
|
+
body = await callBootstrap({ workspaceName })
|
|
122
|
+
console.log(`Workspace "${workspaceName}" creado.`)
|
|
123
|
+
}
|
|
88
124
|
|
|
89
|
-
const { workspaceId, token,
|
|
125
|
+
const { workspaceId, token, projects } = body
|
|
126
|
+
// La API de producción anterior a ORQ-388 no manda workspaceName.
|
|
127
|
+
const wsLabel = body.workspaceName ? ` del workspace "${body.workspaceName}"` : ''
|
|
90
128
|
let defaultProjectId
|
|
91
129
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
130
|
+
const createProject = async (name) => {
|
|
131
|
+
const res2 = await fetch(`${apiBase}/v1/projects?workspaceId=${workspaceId}`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ name }),
|
|
135
|
+
})
|
|
136
|
+
const newProject = await res2.json()
|
|
137
|
+
if (!res2.ok) throw new Error(newProject?.error || `HTTP ${res2.status}`)
|
|
138
|
+
console.log(`Proyecto "${newProject.name}"${wsLabel} creado y configurado como default.`)
|
|
139
|
+
return newProject.id
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (projects.length === 0) {
|
|
143
|
+
console.log(`\nAhora crea el proyecto${wsLabel} que este MCP va a usar (puedes agregar más después).`)
|
|
144
|
+
defaultProjectId = await createProject(await askName('Nombre del proyecto: '))
|
|
95
145
|
} else {
|
|
96
|
-
console.log(
|
|
146
|
+
console.log(`\nElige el proyecto${wsLabel} que este MCP va a usar por default:`)
|
|
97
147
|
console.log(' 0. (crear un proyecto nuevo)')
|
|
98
148
|
projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`))
|
|
99
149
|
const singleDefaultIdx = projects.length === 1 ? 1 : null
|
|
@@ -105,20 +155,10 @@ export async function runSetup() {
|
|
|
105
155
|
if (Number.isNaN(idx) || idx < 0 || idx > projects.length) throw new Error('Selección inválida')
|
|
106
156
|
|
|
107
157
|
if (idx === 0) {
|
|
108
|
-
|
|
109
|
-
if (!name.trim()) throw new Error('El nombre no puede estar vacío')
|
|
110
|
-
const res2 = await fetch(`${apiBase}/v1/projects?workspaceId=${workspaceId}`, {
|
|
111
|
-
method: 'POST',
|
|
112
|
-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
113
|
-
body: JSON.stringify({ name: name.trim() }),
|
|
114
|
-
})
|
|
115
|
-
const newProject = await res2.json()
|
|
116
|
-
if (!res2.ok) throw new Error(newProject?.error || `HTTP ${res2.status}`)
|
|
117
|
-
defaultProjectId = newProject.id
|
|
118
|
-
console.log(`Proyecto "${newProject.name}" creado y configurado como default.`)
|
|
158
|
+
defaultProjectId = await createProject(await askName('Nombre del proyecto nuevo: '))
|
|
119
159
|
} else {
|
|
120
160
|
defaultProjectId = projects[idx - 1].id
|
|
121
|
-
console.log(`Usando tu proyecto "${projects[idx - 1].name}" como default.`)
|
|
161
|
+
console.log(`Usando tu proyecto "${projects[idx - 1].name}"${wsLabel} como default.`)
|
|
122
162
|
}
|
|
123
163
|
}
|
|
124
164
|
|
|
@@ -141,6 +181,6 @@ export async function runSetup() {
|
|
|
141
181
|
console.log(' Claude Desktop -- agrega esto a tu claude_desktop_config.json:')
|
|
142
182
|
console.log(' { "mcpServers": { "orquestra": { "command": "npx", "args": ["orquestra-mcp"] } } }\n')
|
|
143
183
|
console.log(' ¿Usas Cursor, ChatGPT/Codex o Gemini CLI? La config de cada uno está en:')
|
|
144
|
-
console.log(' https://orquestra.me/mcp#instalacion\n')
|
|
145
|
-
console.log('Docs completas y las
|
|
184
|
+
console.log(' https://www.orquestra.me/mcp#instalacion\n')
|
|
185
|
+
console.log('Docs completas y todas las tools disponibles: https://www.orquestra.me/mcp')
|
|
146
186
|
}
|