orquestra-mcp 1.17.0 → 1.18.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.17.0",
3
+ "version": "1.18.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": {
package/src/server.mjs CHANGED
@@ -508,6 +508,7 @@ export function createServer() {
508
508
  categoryId: z.string().optional(),
509
509
  moduleId: z.string().describe('OBLIGATORIO -- módulo al que pertenece esta tarea, para poder agruparla/revisarla después. Si no existe uno relacionado, llamá primero a list_modules (para no duplicar) y create_module.'),
510
510
  dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').describe('OBLIGATORIO -- fecha estimada de término (YYYY-MM-DD), calculada con criterio real según prioridad/tamaño del trabajo (mismo criterio que la "revisión de sincronización" de get_sync_report), nunca un valor arbitrario.'),
511
+ reviewDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato esperado: YYYY-MM-DD').optional().describe('Fecha para volver a MIRAR esta tarea -- recordatorio, NO un entregable. Úsalo para tareas de espera sin deadline real (ej. "esperar respuesta de Apple y revisar en 5 días"): en ese caso NO llenes dueDate, llena reviewDate. Distinto de dueDate (fecha de término real de un entregable) -- una tarea puede tener ambos, uno solo, o ninguno. Dispara un aviso (notificación + correo) al assignedTo el día que llega, un solo disparo.'),
511
512
  assignedTo: z.string().optional().describe('uid del miembro asignado'),
512
513
  tags: z.array(z.string()).optional(),
513
514
  scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
@@ -543,6 +544,7 @@ export function createServer() {
543
544
  categoryId: z.string().optional(),
544
545
  moduleId: z.string().optional(),
545
546
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
547
+ reviewDate: z.string().optional().describe('YYYY-MM-DD. Ver create_task -- recordatorio para volver a revisar, distinto de dueDate. Mandá "" para quitarlo.'),
546
548
  assignedTo: z.string().optional(),
547
549
  tags: z.array(z.string()).optional(),
548
550
  scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
@@ -620,16 +622,17 @@ export function createServer() {
620
622
  server.registerTool(
621
623
  'connect_postproxy_channel',
622
624
  {
623
- description: 'Inicia la conexión de un canal (facebook/instagram/tiktok/linkedin) a Postproxy. Devuelve UNA de dos formas: {url} -- OAuth hosteado por Postproxy, el usuario tiene que abrirlo y completarlo a mano (el agente no puede terminar un OAuth por su cuenta), pásale esa URL tal cual; o {needsPageSelection, profileId, pages} -- pasa con facebook y linkedin, cuando ya hay un login de esa red conectado en la cuenta (un solo login da acceso a varias Pages de Facebook, o al perfil personal y las Organization Pages de LinkedIn, no hace falta repetir el OAuth) y hace falta elegir cuál corresponde a este canal. Esa elección hoy es un flujo de la web (Cuentas conectadas) -- si te llega `needsPageSelection`, dile al usuario que complete la selección ahí, esta tool no tiene una contraparte para elegir la página. Solo para los tipos que soporta Postproxy -- un canal newsletter (Buttondown) se conecta desde la web, no con esta tool.',
625
+ description: 'Inicia la conexión de un canal (facebook/instagram/tiktok/linkedin) a Postproxy. Devuelve UNA de dos formas: {url} -- OAuth hosteado por Postproxy, el usuario tiene que abrirlo y completarlo a mano (el agente no puede terminar un OAuth por su cuenta), pásale esa URL tal cual; o {needsPageSelection, profileId, profileLabel, pages} -- pasa con facebook y linkedin, cuando ya hay un login de esa red conectado en la cuenta (un solo login da acceso a varias Pages de Facebook, o al perfil personal y las Organization Pages de LinkedIn, no hace falta repetir el OAuth) y hace falta elegir cuál corresponde a este canal. Esa elección hoy es un flujo de la web (Cuentas conectadas) -- si te llega `needsPageSelection`, dile al usuario que complete la selección ahí, esta tool no tiene una contraparte para elegir la página. Modelo real de Postproxy: cada perfil (login) vive en UN solo profile group y Orquestra usa un group por proyecto -- el OAuth de esta tool deja el perfil nuevo en el group de este proyecto; una cuenta de Instagram/TikTok conectada desde otro proyecto NO se puede reusar acá (sería publicar en la cuenta de otro proyecto), hay que conectar la cuenta correcta. Solo para los tipos que soporta Postproxy -- un canal newsletter (Buttondown) se conecta desde la web, no con esta tool.',
624
626
  inputSchema: {
625
627
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
626
628
  channelId: z.string().describe('Canal a conectar (ver list_channels) -- su `type` tiene que ser facebook/instagram/tiktok/linkedin'),
627
629
  platform: z.string().describe('Plataforma tal cual la espera Postproxy (mismo valor que channel.type -- ej. "instagram")'),
630
+ forceOAuth: z.boolean().optional().describe('true = iniciar un OAuth nuevo aunque ya exista un login de esa red en la cuenta de Postproxy (conectar OTRA cuenta de Facebook/LinkedIn, o reconectar una vencida). Sin esto, para facebook/linkedin se reusa el login existente y se devuelve needsPageSelection.'),
628
631
  },
629
632
  },
630
- async ({ projectId, channelId, platform }) => {
633
+ async ({ projectId, channelId, platform, forceOAuth }) => {
631
634
  try {
632
- const data = await apiPost(config, `/v1/channels/${channelId}/connect-postproxy`, { projectId: resolveProjectId(projectId), platform })
635
+ const data = await apiPost(config, `/v1/channels/${channelId}/connect-postproxy`, { projectId: resolveProjectId(projectId), platform, ...(forceOAuth ? { forceOAuth: true } : {}) })
633
636
  return textResult(data)
634
637
  } catch (err) {
635
638
  return errorResult(err)
@@ -640,15 +643,16 @@ export function createServer() {
640
643
  server.registerTool(
641
644
  'publish_content_postproxy',
642
645
  {
643
- description: 'Publica o programa una pieza de Contenido en redes sociales vía Postproxy -- solo funciona si la pieza tiene al menos una variante en estatus "approved" y su canal ya está conectado (ver connect_postproxy_channel/list_channels). Respeta approvalRequired -- no la llames para saltarte una aprobación pendiente.',
646
+ description: 'Publica o programa una pieza de Contenido en redes sociales vía Postproxy -- solo funciona si la pieza tiene al menos una variante en estatus "approved" y su canal ya está conectado (ver connect_postproxy_channel/list_channels). Respeta approvalRequired -- no la llames para saltarte una aprobación pendiente. Cada variante aprobada va en SU PROPIO post de Postproxy (caption/media/fecha propios); antes de cada uno el server verifica el perfil EN VIVO y rechaza con un 400 claro si la cuenta está vencida, ya no existe, o (Instagram/TikTok) vive en el group de Postproxy de otro proyecto -- es decir, el canal apunta a la cuenta de otro proyecto y hay que reconectarlo. Devuelve {posts:[{channelId, postproxyPostId, status}], failed:[{channelId, channelName, error}]}: con éxito parcial responde 200 igual, así que revisa `failed` y cuéntaselo al usuario -- cada variante queda con su externalGuid o su publishError en la pieza.',
644
647
  inputSchema: {
645
648
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
646
649
  contentId: z.string().describe('Pieza de Contenido a publicar (ver list_content)'),
650
+ channelId: z.string().optional().describe('Publica solo la variante de ese canal. Sin channelId se publican todas las variantes aprobadas de la pieza, cada una en su propio post.'),
647
651
  },
648
652
  },
649
- async ({ projectId, contentId }) => {
653
+ async ({ projectId, contentId, channelId }) => {
650
654
  try {
651
- const data = await apiPost(config, `/v1/content/${contentId}/publish-postproxy`, { projectId: resolveProjectId(projectId) })
655
+ const data = await apiPost(config, `/v1/content/${contentId}/publish-postproxy`, { projectId: resolveProjectId(projectId), ...(channelId ? { channelId } : {}) })
652
656
  return textResult(data)
653
657
  } catch (err) {
654
658
  return errorResult(err)
@@ -1136,7 +1140,7 @@ export function createServer() {
1136
1140
  )
1137
1141
 
1138
1142
  registerRead('list_changelog', '/v1/changelog',
1139
- 'Lista TODAS las entradas de changelog de un proyecto, con todos sus campos (id, section, text, version, distributions, repo, taskId, createdAt, createdBy) -- a diferencia de get_dashboard_summary/get_project_context, que solo traen un resumen recortado (texto/título, sin repo ni distributions). Útil para auditar (ej. "¿qué entradas viejas no tienen repo?") antes de update_changelog_entry.',
1143
+ 'Lista TODAS las entradas de changelog de un proyecto, con todos sus campos (id, section, text, version, distributions, repo, taskId, moduleId, createdAt, createdBy) -- a diferencia de get_dashboard_summary/get_project_context, que solo traen un resumen recortado (texto/título, sin repo ni distributions). Útil para auditar (ej. "¿qué entradas viejas no tienen repo?") antes de update_changelog_entry.',
1140
1144
  {}, (args, projectId) => ({ projectId }))
1141
1145
 
1142
1146
  registerCreate(
@@ -1153,12 +1157,13 @@ export function createServer() {
1153
1157
  })).optional().describe('Distribuciones a las que aplica esta entrada (ver list_distributions) -- vacío/omitido = aplica a todo el proyecto. Cada una puede tener su propia versión dentro de esta entrada.'),
1154
1158
  repo: z.string().optional().describe('"owner/name" del repo de origen del commit/PR que generó esta entrada, si se sabe -- alimenta el chip de repo en la tarjeta de versión.'),
1155
1159
  taskId: z.string().optional().describe('Tarea que esta entrada documenta o cierra, si se sabe (ver list_tasks) -- alimenta el chip de tarea, clicable al detalle, en la tarjeta de versión.'),
1160
+ moduleId: z.string().optional().describe('Módulo al que pertenece esta entrada, si se sabe (ver list_modules). Agregado ORQ-178 -- también se puede llenar después vía accept_suggestion sobre una sugerencia kind="module_match" que generó la reconciliación de orquestra-infra.'),
1156
1161
  plannedReleaseDate: z.string().optional().describe('YYYY-MM-DD -- fecha objetivo de corte de esta versión (solo tiene sentido si la entrada queda en Unreleased). Puramente informativo, alimenta el calendario global -- no dispara release_changelog_version solo.'),
1157
1162
  }
1158
1163
  )
1159
1164
 
1160
1165
  registerUpdate('update_changelog_entry', '/v1/changelog',
1161
- 'Edita una entrada de changelog existente (section/text/version/distributions/repo/taskId) -- primera edición puntual por id que existe (antes solo había create + el bulk-update de release_changelog_version). Pensada sobre todo para backfill/corrección (ej. completar "repo" en entradas viejas que se crearon antes de que ese campo se mandara), no para reescribir historia -- si el usuario solo quiere corregir el texto de una entrada reciente por error de tipeo, esto también sirve.',
1166
+ 'Edita una entrada de changelog existente (section/text/version/distributions/repo/taskId/moduleId) -- primera edición puntual por id que existe (antes solo había create + el bulk-update de release_changelog_version). Pensada sobre todo para backfill/corrección (ej. completar "repo" en entradas viejas que se crearon antes de que ese campo se mandara), no para reescribir historia -- si el usuario solo quiere corregir el texto de una entrada reciente por error de tipeo, esto también sirve.',
1162
1167
  {
1163
1168
  section: z.enum(CHANGELOG_SECTIONS).optional(),
1164
1169
  text: z.string().optional(),
@@ -1169,6 +1174,7 @@ export function createServer() {
1169
1174
  })).optional(),
1170
1175
  repo: z.string().optional().describe('"owner/name" del repo de origen.'),
1171
1176
  taskId: z.string().optional(),
1177
+ moduleId: z.string().optional().describe('Ver create_changelog_entry.'),
1172
1178
  plannedReleaseDate: z.string().optional().describe('YYYY-MM-DD -- ver create_changelog_entry.'),
1173
1179
  }
1174
1180
  )
package/src/setup.mjs CHANGED
@@ -92,15 +92,16 @@ export async function runSetup() {
92
92
  if (created) {
93
93
  defaultProjectId = body.projectId
94
94
  console.log(`Se creó un workspace y un proyecto default para ti ("${projects[0].name}") -- puedes renombrarlos cuando quieras desde orquestra-web.`)
95
- } else if (projects.length === 1) {
96
- defaultProjectId = projects[0].id
97
- console.log(`Usando tu proyecto "${projects[0].name}" como default.`)
98
95
  } else {
99
96
  console.log('\nElige el proyecto que este MCP va a usar por default:')
100
97
  console.log(' 0. (crear un proyecto nuevo)')
101
98
  projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`))
102
- const answer = await prompt('Número: ')
103
- const idx = parseInt(answer, 10)
99
+ const singleDefaultIdx = projects.length === 1 ? 1 : null
100
+ const answer = await prompt(
101
+ singleDefaultIdx ? `Número [Enter para usar "${projects[0].name}"]: ` : 'Número: '
102
+ )
103
+ const trimmed = answer.trim()
104
+ const idx = trimmed === '' && singleDefaultIdx !== null ? singleDefaultIdx : parseInt(trimmed, 10)
104
105
  if (Number.isNaN(idx) || idx < 0 || idx > projects.length) throw new Error('Selección inválida')
105
106
 
106
107
  if (idx === 0) {
@@ -117,6 +118,7 @@ export async function runSetup() {
117
118
  console.log(`Proyecto "${newProject.name}" creado y configurado como default.`)
118
119
  } else {
119
120
  defaultProjectId = projects[idx - 1].id
121
+ console.log(`Usando tu proyecto "${projects[idx - 1].name}" como default.`)
120
122
  }
121
123
  }
122
124