orquestra-mcp 1.50.0 → 1.52.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/bin/orquestra-mcp.mjs +11 -2
- package/package.json +1 -1
- package/src/api.mjs +14 -2
- package/src/configFile.mjs +90 -4
- package/src/server.mjs +18 -9
- package/src/setup.mjs +77 -20
- package/src/updateCheck.mjs +56 -0
package/bin/orquestra-mcp.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { createServer, createSetupNeededServer } from '../src/server.mjs'
|
|
4
4
|
import { runSetup } from '../src/setup.mjs'
|
|
5
5
|
import { runVaultSync } from '../src/vaultSync.mjs'
|
|
6
|
+
import { startUpdateCheck } from '../src/updateCheck.mjs'
|
|
6
7
|
|
|
7
8
|
const HELP_TEXT = `orquestra-mcp -- Servidor MCP de Orquestra (132 tools)
|
|
8
9
|
|
|
@@ -10,6 +11,9 @@ Uso:
|
|
|
10
11
|
npx orquestra-mcp Inicia el servidor MCP por stdio -- lo usa tu cliente
|
|
11
12
|
MCP (Claude Code, Claude Desktop, etc.), no se corre a mano
|
|
12
13
|
npx orquestra-mcp setup Login por navegador + conecta esta carpeta a un proyecto
|
|
14
|
+
npx orquestra-mcp setup --agent codex
|
|
15
|
+
Lo mismo para una herramienta puntual (claude-code, codex,
|
|
16
|
+
cursor, gemini-cli...): cada una firma con su propio agente
|
|
13
17
|
npx orquestra-mcp vault sync <carpeta> [--apply]
|
|
14
18
|
Sincroniza la base de conocimiento del workspace con una
|
|
15
19
|
carpeta de tu vault de Obsidian (la 1ra vez solo muestra el plan)
|
|
@@ -17,7 +21,8 @@ Uso:
|
|
|
17
21
|
|
|
18
22
|
Config (la deja lista "setup"): ~/.orquestra/mcp-config.json
|
|
19
23
|
Alternativa manual (variables de entorno): ORQUESTRA_API_TOKEN, ORQUESTRA_WORKSPACE_ID,
|
|
20
|
-
ORQUESTRA_DEFAULT_PROJECT_ID
|
|
24
|
+
ORQUESTRA_DEFAULT_PROJECT_ID. ORQUESTRA_AGENT elige el token de una herramienta
|
|
25
|
+
(ej. codex) cuando la carpeta tiene varias conectadas.
|
|
21
26
|
|
|
22
27
|
Docs completas: https://orquestra.me/mcp
|
|
23
28
|
`
|
|
@@ -31,7 +36,8 @@ async function main() {
|
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
if (cmd === 'setup') {
|
|
34
|
-
|
|
39
|
+
const agentIdx = process.argv.indexOf('--agent')
|
|
40
|
+
await runSetup({ agent: agentIdx > 0 ? process.argv[agentIdx + 1] : process.env.ORQUESTRA_AGENT })
|
|
35
41
|
return
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -45,6 +51,9 @@ async function main() {
|
|
|
45
51
|
return
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
// Sin await: el aviso de versión nueva no debe atrasar el arranque.
|
|
55
|
+
startUpdateCheck()
|
|
56
|
+
|
|
48
57
|
let server
|
|
49
58
|
try {
|
|
50
59
|
server = createServer()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.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": {
|
package/src/api.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readConfigFile, resolveProjectForCwd, CONFIG_PATH } from './configFile.mjs'
|
|
1
|
+
import { readConfigFile, resolveProjectForCwd, entryForAgent, agentKey, CONFIG_PATH } from './configFile.mjs'
|
|
2
2
|
import { request } from './http.mjs'
|
|
3
3
|
|
|
4
4
|
const DEFAULT_API_BASE = 'https://api.orquestra.me'
|
|
@@ -8,7 +8,19 @@ export function loadConfig() {
|
|
|
8
8
|
// Todo sale de la MISMA carpeta mapeada (ver resolveProjectForCwd) --
|
|
9
9
|
// apiToken/workspaceId/projectId siempre son de una sola cuenta, nunca
|
|
10
10
|
// el token de una carpeta con el proyecto de otra.
|
|
11
|
-
const
|
|
11
|
+
const folderEntry = resolveProjectForCwd(fileConfig?.projects, process.cwd())
|
|
12
|
+
// ORQUESTRA_AGENT (ORQ-415) elige el token de esa herramienta en la
|
|
13
|
+
// carpeta. Si no está conectada falla explícito: caer al token default
|
|
14
|
+
// haría que sus cambios queden firmados por otra herramienta.
|
|
15
|
+
const agent = agentKey(process.env.ORQUESTRA_AGENT)
|
|
16
|
+
const folderConfig = agent ? entryForAgent(folderEntry, agent) : folderEntry
|
|
17
|
+
if (agent && folderEntry && !folderConfig && !process.env.ORQUESTRA_API_TOKEN) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`La herramienta "${process.env.ORQUESTRA_AGENT}" (ORQUESTRA_AGENT) no está conectada en esta carpeta (${process.cwd()}), ` +
|
|
20
|
+
`solo otras. Para que sus cambios queden a su nombre y no al de otra herramienta, abre una terminal aquí, ` +
|
|
21
|
+
`corre "npx orquestra-mcp@latest setup" y elige esa herramienta; después reconecta el MCP.`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
const apiBase = process.env.ORQUESTRA_API_BASE || folderConfig?.apiBase || DEFAULT_API_BASE
|
|
14
26
|
const apiToken = process.env.ORQUESTRA_API_TOKEN || folderConfig?.apiToken
|
package/src/configFile.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
-
import { homedir } from 'os'
|
|
2
|
+
import { homedir, hostname } from 'os'
|
|
3
|
+
import { randomBytes } from 'crypto'
|
|
3
4
|
import { join, dirname, win32, posix } from 'path'
|
|
4
5
|
|
|
5
6
|
export const CONFIG_PATH = join(homedir(), '.orquestra', 'mcp-config.json')
|
|
@@ -45,19 +46,104 @@ export function writeConfigFile(data) {
|
|
|
45
46
|
writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 0o600 })
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// Identidad por herramienta (ORQ-415): cada herramienta (Claude Code, Codex,
|
|
50
|
+
// Cursor...) tiene su propio token -- su propio agente en Orquestra -- aunque
|
|
51
|
+
// trabajen en la misma carpeta. La entrada de la carpeta guarda la conexión
|
|
52
|
+
// "default" arriba (la que se usa sin ORQUESTRA_AGENT) con `agent` = su
|
|
53
|
+
// herramienta, y todas en `agents[clave]`. Una entrada de antes de ORQ-415 no
|
|
54
|
+
// trae `agent`: su token es el de "Claude Code", el default histórico.
|
|
55
|
+
// Mismo slug que agentSlug() de bootstrap.mjs en orquestra-infra.
|
|
56
|
+
export function agentKey(label) {
|
|
57
|
+
return String(label || '')
|
|
58
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
59
|
+
.toLowerCase().trim()
|
|
60
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
61
|
+
.replace(/^-+|-+$/g, '')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const LEGACY_AGENT = 'claude-code'
|
|
65
|
+
const defaultAgentOf = entry => entry?.agent || (entry?.apiToken ? LEGACY_AGENT : null)
|
|
66
|
+
const connectionOf = ({ apiBase, apiToken, workspaceId, projectId }) => ({ apiBase, apiToken, workspaceId, projectId })
|
|
67
|
+
|
|
68
|
+
// La conexión de UNA herramienta dentro de la entrada de una carpeta, o null
|
|
69
|
+
// si esa herramienta nunca se conectó ahí (nunca cae al token de otra: eso
|
|
70
|
+
// es justo lo que hacía firmar a Codex como Claude Code).
|
|
71
|
+
export function entryForAgent(entry, key) {
|
|
72
|
+
if (!entry || !key) return entry
|
|
73
|
+
if (entry.agents?.[key]) return entry.agents[key]
|
|
74
|
+
return defaultAgentOf(entry) === key ? connectionOf(entry) : null
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
// Guarda/actualiza la cuenta+proyecto de UNA carpeta sin pisar las demás --
|
|
49
78
|
// setup se puede correr varias veces, una por cada carpeta contenedora de
|
|
50
79
|
// repos (ver resolveProjectForCwd), y cada corrida solo debe tocar su
|
|
51
80
|
// propia entrada -- incluida la cuenta (apiToken/workspaceId), para poder
|
|
52
81
|
// tener una carpeta en una cuenta de Orquestra y otra carpeta en otra
|
|
53
|
-
// cuenta distinta al mismo tiempo.
|
|
54
|
-
|
|
82
|
+
// cuenta distinta al mismo tiempo. Con `agent` solo toca la conexión de esa
|
|
83
|
+
// herramienta: la default de la carpeta cambia únicamente si era la misma
|
|
84
|
+
// herramienta (o la carpeta no tenía ninguna).
|
|
85
|
+
export function upsertProjectMapping({ apiBase, apiToken, workspaceId, folderPath, projectId, agent }) {
|
|
55
86
|
const existing = readConfigFile() || { projects: {} }
|
|
56
87
|
const projects = { ...(existing.projects || {}) }
|
|
57
|
-
|
|
88
|
+
const conn = { apiBase, apiToken, workspaceId, projectId }
|
|
89
|
+
if (!agent) {
|
|
90
|
+
projects[folderPath] = conn
|
|
91
|
+
} else {
|
|
92
|
+
const prev = projects[folderPath]
|
|
93
|
+
const agents = { ...(prev?.agents || {}), [agent]: conn }
|
|
94
|
+
const keepDefault = prev && defaultAgentOf(prev) && defaultAgentOf(prev) !== agent
|
|
95
|
+
projects[folderPath] = keepDefault
|
|
96
|
+
? { ...connectionOf(prev), agent: defaultAgentOf(prev), agents }
|
|
97
|
+
: { ...conn, agent, agents }
|
|
98
|
+
}
|
|
58
99
|
writeConfigFile({ projects })
|
|
59
100
|
}
|
|
60
101
|
|
|
102
|
+
// El setup de una herramienta ya conocida en esta máquina revoca su token
|
|
103
|
+
// anterior (bootstrap en orquestra-infra): hay que ponerle el nuevo a TODAS
|
|
104
|
+
// las carpetas donde esa herramienta usa el mismo workspace, no solo a la del
|
|
105
|
+
// setup, o las demás quedarían con un token muerto. Devuelve las carpetas.
|
|
106
|
+
export function replaceAgentToken({ workspaceId, agent, apiToken }) {
|
|
107
|
+
const existing = readConfigFile()
|
|
108
|
+
if (!existing?.projects) return []
|
|
109
|
+
const updated = []
|
|
110
|
+
for (const [folderPath, entry] of Object.entries(existing.projects)) {
|
|
111
|
+
let touched = false
|
|
112
|
+
if (defaultAgentOf(entry) === agent && entry.workspaceId === workspaceId) { entry.apiToken = apiToken; touched = true }
|
|
113
|
+
const own = entry.agents?.[agent]
|
|
114
|
+
if (own && own.workspaceId === workspaceId) { own.apiToken = apiToken; touched = true }
|
|
115
|
+
if (touched) updated.push(folderPath)
|
|
116
|
+
}
|
|
117
|
+
if (updated.length) writeConfigFile(existing)
|
|
118
|
+
return updated
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Herramientas ya conectadas en esta máquina (en cualquier carpeta).
|
|
122
|
+
export function connectedAgents() {
|
|
123
|
+
const keys = new Set()
|
|
124
|
+
for (const entry of Object.values(readConfigFile()?.projects || {})) {
|
|
125
|
+
if (defaultAgentOf(entry)) keys.add(defaultAgentOf(entry))
|
|
126
|
+
for (const key of Object.keys(entry.agents || {})) keys.add(key)
|
|
127
|
+
}
|
|
128
|
+
return keys
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Identidad de la máquina (ORQ-415): un id aleatorio, no el serial ni la MAC
|
|
132
|
+
// -- solo distingue esta instalación ("Codex · MacBook-Pro" vs "Codex ·
|
|
133
|
+
// Mac-mini"). Se crea una vez y se reusa en cada setup.
|
|
134
|
+
export const MACHINE_PATH = join(homedir(), '.orquestra', 'machine.json')
|
|
135
|
+
|
|
136
|
+
export function readOrCreateMachine() {
|
|
137
|
+
try {
|
|
138
|
+
const saved = JSON.parse(readFileSync(MACHINE_PATH, 'utf8'))
|
|
139
|
+
if (/^[a-f0-9]{16,64}$/.test(saved.id)) return { id: saved.id, name: saved.name || hostname().replace(/\.local$/, '') }
|
|
140
|
+
} catch {}
|
|
141
|
+
const machine = { id: randomBytes(8).toString('hex'), name: hostname().replace(/\.local$/, '') }
|
|
142
|
+
mkdirSync(dirname(MACHINE_PATH), { recursive: true })
|
|
143
|
+
writeFileSync(MACHINE_PATH, JSON.stringify(machine, null, 2), { mode: 0o600 })
|
|
144
|
+
return machine
|
|
145
|
+
}
|
|
146
|
+
|
|
61
147
|
// Busca, subiendo desde cwd hacia la raíz, la carpeta mapeada más específica
|
|
62
148
|
// -- mismo criterio que .editorconfig/.gitconfig: el ancestro más cercano
|
|
63
149
|
// gana, así que un repo individual con su propio mapeo (y su propia cuenta)
|
package/src/server.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import { loadConfig, apiGet, apiPost, apiPatch, apiPatchPath, apiDelete, apiDeletePath, putToPresignedUrl, ApiError } from './api.mjs'
|
|
7
7
|
import { cancelContext } from './http.mjs'
|
|
8
|
+
import { updateNotice } from './updateCheck.mjs'
|
|
8
9
|
import { STAGES, PLAYBOOKS } from './stages.mjs'
|
|
9
10
|
import { FOUNDATIONS } from './foundations.mjs'
|
|
10
11
|
import { PLATFORM_PLAYBOOKS, TRACKING_TOOLS } from './platformPlaybooks.mjs'
|
|
@@ -104,7 +105,7 @@ const SETUP_GUIDE = {
|
|
|
104
105
|
// preguntar en vez de adivinar.
|
|
105
106
|
const SERVER_INSTRUCTIONS = `Este server conecta con Orquestra, la app de gestión de proyectos del usuario -- úsalo para mantener su proyecto sincronizado con el trabajo real, sin que tenga que pedírtelo cada vez.
|
|
106
107
|
|
|
107
|
-
Al empezar una sesión de trabajo en este proyecto, llama una vez a get_project_context para saber en qué fase está, qué tareas hay abiertas/vencidas/bloqueadas y qué se hizo recientemente. Úsalo como contexto de fondo, no lo repitas en cada mensaje.
|
|
108
|
+
Al empezar una sesión de trabajo en este proyecto, llama una vez a get_project_context para saber en qué fase está, qué tareas hay abiertas/vencidas/bloqueadas y qué se hizo recientemente. Úsalo como contexto de fondo, no lo repitas en cada mensaje. Si trae orquestraMcpUpdate, avísale al usuario UNA sola vez en la sesión que hay una versión nueva de este MCP y cómo actualizar (usa su campo message), sin interrumpir lo que estaba pidiendo.
|
|
108
109
|
|
|
109
110
|
Si get_project_context devuelve un proyecto vacío (sin módulos ni tareas), es la primera vez que se usa este MCP aquí -- pero "vacío en Orquestra" NO significa "recién empezado en la realidad". Antes de ofrecer nada, pregúntale al usuario: ¿este proyecto es una idea nueva, o ya existe y solo lo estás conectando por primera vez? Según la respuesta:
|
|
110
111
|
- Si es una idea nueva: llama a get_setup_guide y ofrece un playbook de etapa "pre" (get_stage_playbook + apply_stage_playbook) -- ahí sí basta con eso, el proyecto de verdad no tiene nada más que sincronizar todavía.
|
|
@@ -133,6 +134,7 @@ Durante el trabajo normal de código, sin que el usuario te lo pida explícitame
|
|
|
133
134
|
- Paridad por producto: las distribuciones que son el mismo producto en distintas plataformas (típicamente Web/iOS/Android) comparten un productKey (list_distributions lo muestra). Si al crear una tarea con scope en una de ellas la respuesta trae parityAddedDistIds, el server sumó las hermanas como pendientes -- dícelo al usuario ("también quedó pendiente en iOS y Android") y no lo deshagas salvo que el trabajo sea genuinamente de una sola plataforma (entonces skipParity:true, o quítalas con update_task/scope explicando por qué). Al cerrar una tarea con scope multiplataforma, marca done:true SOLO en la plataforma donde de verdad se hizo (update_task/scope) -- el estatus "Listo" no mueve esos flags solo; cuando un commit o PR del repo de otra plataforma menciona el código de la tarea (ej. "(ORQ-123)"), el servidor propone solo una sugerencia scope_done_proposal (list_suggestions) para marcar esa plataforma como hecha: si la ves, confirma con el código real y acéptala con accept_suggestion en vez de editar el scope a mano. Si el trabajo de otra plataforma ya vive en OTRA tarea (una tarea gemela, ej. "[iOS] ..." y "[Android] ..."), no dupliques scope: liga la hermana con update_task parityOf=<id de la tarea origen> y en paridad cuentan como un solo item; al cerrar la hermana su plataforma queda hecha sola. Dejar las hermanas en done:false es exactamente lo que hace que el desfase aparezca en get_parity_summary y en la revisión de sincronización (tasksClosedWithParityPending), que es el punto: que nunca se olvide el trabajo del otro lado. Si el proyecto tiene 2+ plataformas y ninguna tiene productKey (get_parity_summary → productGroups.configured=false), pregúntale al usuario cuáles son el mismo producto y configúralo con update_distribution -- nunca lo infieras solo del tipo de plataforma.
|
|
134
135
|
- Al crear o editar una tarea (o un módulo), llena TODO campo para el que tengas un dato real, no solo title/moduleId/dueDate/comment: type (no lo dejes en el default "feature" si en realidad es un bug/mejora/doc/etc.), tags, stack (tecnologías que genuinamente tocaste), scope (paridad multiplataforma, si aplica), y los campos de código (branch/prNumber/prUrl/prStatus/commitCount/resolvedCommitSha/resolvedCommitMessage/resolvedCommitUrl/resolvedCommitAt) cuando de verdad sepas esos valores (el branch en el que estás trabajando, el PR que abriste, el commit que resolvió la tarea). No dejes un campo en blanco solo porque no es obligatorio -- pero esto no es licencia para inventar: si no tenés el dato real (ej. no hay branch/PR porque el proyecto commitea directo a main), dejalo vacío en vez de rellenarlo con algo plausible pero falso -- mismo criterio que ya se pedía para resolvedCommitSha.
|
|
135
136
|
- Si una tarea tiene más de una actividad o paso concreto, no los amontones como texto corrido en description -- usa el checklist (add_checklist_item, un ítem por actividad, opcionalmente con responsable) para que cada paso se pueda marcar independientemente. description queda para el contexto/propósito general de la tarea (el qué y el por qué), no como lista de pasos. Si estás editando una tarea existente cuya description ya creció así, migra esos puntos a checklist en vez de seguir agregando ahí.
|
|
137
|
+
- Una tarea = UN entregable; un ítem del checklist = UN paso corto de ese entregable. Si el título junta varias cosas con comas o "y" ("X, Y y Z"), o un ítem del checklist tendría su propio deploy, commits, pruebas o alertas (es una integración/función entera, no un paso), son tareas separadas: créalas aparte en el mismo módulo (si son muchas del mismo tema, ese tema es un módulo, no una tarea con 14 ítems). Lo que depende de algo externo o futuro (un contrato, un proveedor, una versión que todavía no sale, la respuesta de una tienda) va en su propia tarea -- en Bloqueado o con reviewDate --, nunca dentro de una tarea que ya está lista, porque la deja abierta indefinidamente. El texto de un ítem es la acción ("Configurar cron diario de la CSL"), no la bitácora: commits, "probado en vivo", tiempos y tamaños van en un comment de la tarea, en resolvedCommitSha o en el changelog. Algo que ya no aplica no se deja como ítem pendiente con "ya no hace falta" en el texto: díselo al usuario y bórralo con delete_checklist_item (con un comment en la tarea explicando por qué). No reorganices tareas que ya existen así salvo que el usuario lo pida -- la regla es para lo nuevo.
|
|
136
138
|
- Al cerrar una tarea en la que trabajaste de verdad (o al final de una sesión de trabajo real sobre ella), registrá las horas con log_time -- el tiempo REAL que llevó ese trabajo en esta sesión (aproximalo con honestidad: minutos de trabajo real, no la duración del chat ni la estimación de la tarea), con una nota corta de qué se hizo. Ese registro alimenta "Horas del equipo" en Inversión de Contenido y es la única forma de que el costo real del proyecto no dependa de que el usuario lo cargue a mano. No lo inventes ni lo infles; si genuinamente no podés estimar el tiempo de la sesión, no registres nada.
|
|
137
139
|
- No todas las tareas necesitan una corrida de QA -- clasifica antes de disparar una: cambios que afectan comportamiento real (features, bugs, UI, lógica de negocio, endpoints/API) sí la necesitan; cambios que no alteran comportamiento (docs, comentarios, refactors sin cambio funcional, config/tooling interno, notas/reuniones/ideas) no. Si decides que sí, revisa primero con list_agents que exista un agente (kind="qa") para el stack que tocaste (si no hay ninguno, créalo con create_agent -- necesita una máquina ya emparejada, ver Workspace → Máquinas) y llama a create_agent_run en la rama real de ese trabajo (manda taskId si el trabajo era de una tarea puntual -- si el agente tiene la revisión con IA activada, la acota a eso en vez de "revisa todo el proyecto"), dejando la tarea en "En revisión" -- NUNCA la muevas a "Listo" ni digas que "QA lo revisó" sin haber confirmado con get_agent_run un estatus terminal "passed" de verdad (las corridas son asíncronas, tardan minutos en otra máquina). Si tarda, dile al usuario que quedó en cola (con el runId) y que confirmas cuando termine -- no lo des por hecho nunca.
|
|
138
140
|
|
|
@@ -175,7 +177,10 @@ export function createSetupNeededServer(reason) {
|
|
|
175
177
|
server.registerTool('diagnostico_conexion', {
|
|
176
178
|
description: 'Explica por qué Orquestra no está conectado en esta carpeta y cómo arreglarlo.',
|
|
177
179
|
inputSchema: {},
|
|
178
|
-
}, async () =>
|
|
180
|
+
}, async () => {
|
|
181
|
+
const update = updateNotice(PKG_VERSION)
|
|
182
|
+
return { content: [{ type: 'text', text: `orquestra-mcp ${PKG_VERSION}: ${reason}${update ? `\n\n${update.message}` : ''}` }] }
|
|
183
|
+
})
|
|
179
184
|
return server
|
|
180
185
|
}
|
|
181
186
|
|
|
@@ -205,14 +210,14 @@ export function createServer() {
|
|
|
205
210
|
return projectId
|
|
206
211
|
}
|
|
207
212
|
|
|
208
|
-
function registerRead(name, path, description, extraSchema, buildParams) {
|
|
213
|
+
function registerRead(name, path, description, extraSchema, buildParams, decorate = data => data) {
|
|
209
214
|
server.registerTool(
|
|
210
215
|
name,
|
|
211
216
|
{ description, inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC), ...extraSchema } },
|
|
212
217
|
async (args) => {
|
|
213
218
|
try {
|
|
214
219
|
const data = await apiGet(config, path, buildParams(args, resolveProjectId(args.projectId)))
|
|
215
|
-
return textResult(data)
|
|
220
|
+
return textResult(decorate(data))
|
|
216
221
|
} catch (err) {
|
|
217
222
|
return errorResult(err)
|
|
218
223
|
}
|
|
@@ -264,7 +269,11 @@ export function createServer() {
|
|
|
264
269
|
'/v1/context',
|
|
265
270
|
'Resumen compacto del estado de un proyecto de Orquestra: fase, plataformas, tareas abiertas/vencidas/bloqueadas, módulos, ideas y changelog reciente.',
|
|
266
271
|
{},
|
|
267
|
-
(args, projectId) => ({ projectId })
|
|
272
|
+
(args, projectId) => ({ projectId }),
|
|
273
|
+
data => {
|
|
274
|
+
const update = updateNotice(PKG_VERSION)
|
|
275
|
+
return update ? { ...data, orquestraMcpUpdate: update } : data
|
|
276
|
+
}
|
|
268
277
|
)
|
|
269
278
|
|
|
270
279
|
registerRead(
|
|
@@ -295,10 +304,10 @@ export function createServer() {
|
|
|
295
304
|
registerRead(
|
|
296
305
|
'search',
|
|
297
306
|
'/v1/search',
|
|
298
|
-
'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y
|
|
307
|
+
'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas, docs, riesgos, módulos, contenido y distribuciones de un proyecto de Orquestra -- en los mismos campos que pueden tener [[wikilinks]] (descripción, mitigación, criterios de cierre, puntos, verificaciones...). También por folio ("ORQ-123") o por clave de módulo ("AUTH-3") -- la exacta sale primero -- y sin importar acentos. Primero lo que coincide en el folio o el título, después en el cuerpo; tope de 30 resultados: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
|
|
299
308
|
{
|
|
300
309
|
q: z.string().describe('Texto a buscar'),
|
|
301
|
-
types: z.array(z.enum(['task', 'activity', 'note', 'idea', 'doc', 'knowledge'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en todos. knowledge = solo la base vinculada a este proyecto.'),
|
|
310
|
+
types: z.array(z.enum(['task', 'activity', 'note', 'idea', 'doc', 'knowledge', 'risk', 'module', 'content', 'distribution'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en todos. knowledge = solo la base vinculada a este proyecto.'),
|
|
302
311
|
},
|
|
303
312
|
(args, projectId) => ({ projectId, q: args.q, types: args.types?.join(',') })
|
|
304
313
|
)
|
|
@@ -751,7 +760,7 @@ export function createServer() {
|
|
|
751
760
|
registerCreate(
|
|
752
761
|
'create_task',
|
|
753
762
|
'/v1/tasks',
|
|
754
|
-
'Crea una tarea en un proyecto de Orquestra. moduleId y dueDate son OBLIGATORIOS -- revisa list_modules primero (si ninguno aplica, creá uno con create_module antes de crear la tarea, nunca la dejes sin agrupar); dueDate se calcula con criterio real (prioridad/tamaño del trabajo), nunca un valor arbitrario. Una reunión, llamada o seguimiento NO es tarea: usa log_activity (type "business" sí es tarea: entregable no-código).',
|
|
763
|
+
'Crea una tarea en un proyecto de Orquestra. moduleId y dueDate son OBLIGATORIOS -- revisa list_modules primero (si ninguno aplica, creá uno con create_module antes de crear la tarea, nunca la dejes sin agrupar); dueDate se calcula con criterio real (prioridad/tamaño del trabajo), nunca un valor arbitrario. Una reunión, llamada o seguimiento NO es tarea: usa log_activity (type "business" sí es tarea: entregable no-código). Una tarea = UN entregable: si el título junta varias cosas con comas o "y", crea una tarea por cada una (mismo módulo); lo que espera algo externo o futuro va en su propia tarea, no dentro de otra.',
|
|
755
764
|
{
|
|
756
765
|
relatedTo: RELATED_TO,
|
|
757
766
|
title: z.string().describe('Título de la tarea'),
|
|
@@ -1580,7 +1589,7 @@ export function createServer() {
|
|
|
1580
1589
|
server.registerTool(
|
|
1581
1590
|
'add_checklist_item',
|
|
1582
1591
|
{
|
|
1583
|
-
description: 'Agrega un ítem al checklist de una tarea de Orquestra, opcionalmente con un responsable.',
|
|
1592
|
+
description: 'Agrega un ítem al checklist de una tarea de Orquestra, opcionalmente con un responsable. Un ítem es UN paso corto del entregable de la tarea, escrito como acción. Si lleva su propio deploy, commits, pruebas o alertas, o depende de algo externo (contrato, proveedor, versión futura), es una tarea aparte (create_task), no un ítem. Commits y "probado en vivo" van en un comment de la tarea, no en el texto del ítem.',
|
|
1584
1593
|
inputSchema: {
|
|
1585
1594
|
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1586
1595
|
taskId: z.string().describe('ID de la tarea'),
|
package/src/setup.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { exec } from 'child_process'
|
|
|
4
4
|
import { createInterface } from 'readline'
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
import { resolve } from 'path'
|
|
7
|
-
import { upsertProjectMapping, CONFIG_PATH } from './configFile.mjs'
|
|
7
|
+
import { upsertProjectMapping, replaceAgentToken, connectedAgents, readOrCreateMachine, agentKey, CONFIG_PATH } from './configFile.mjs'
|
|
8
8
|
|
|
9
9
|
const DEFAULT_API_BASE = 'https://api.orquestra.me'
|
|
10
10
|
const DEFAULT_WEB_URL = 'https://app.orquestra.me'
|
|
@@ -15,12 +15,16 @@ function openBrowser(url) {
|
|
|
15
15
|
exec(`${cmd} "${url}"`, () => {})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Herramientas conocidas (ORQ-415): cada una queda como su propio agente en
|
|
19
|
+
// Orquestra ("Codex · MacBook-Pro") y con su propio token en la carpeta.
|
|
20
|
+
const TOOLS = ['Claude Code', 'Codex', 'Cursor', 'Gemini CLI', 'Claude Desktop']
|
|
21
|
+
|
|
18
22
|
function prompt(question) {
|
|
19
23
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
20
24
|
return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer) }))
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export async function runSetup() {
|
|
27
|
+
export async function runSetup({ agent: presetAgent } = {}) {
|
|
24
28
|
const apiBase = process.env.ORQUESTRA_API_BASE || DEFAULT_API_BASE
|
|
25
29
|
const webUrl = process.env.ORQUESTRA_WEB_URL || DEFAULT_WEB_URL
|
|
26
30
|
const state = randomBytes(16).toString('hex')
|
|
@@ -75,8 +79,9 @@ export async function runSetup() {
|
|
|
75
79
|
const idToken = await idTokenPromise
|
|
76
80
|
console.log('Login recibido, configurando...')
|
|
77
81
|
|
|
78
|
-
const
|
|
79
|
-
const agentLabel =
|
|
82
|
+
const machine = readOrCreateMachine()
|
|
83
|
+
const agentLabel = await chooseTool(presetAgent, machine)
|
|
84
|
+
const agent = agentKey(agentLabel)
|
|
80
85
|
|
|
81
86
|
// askIfMissing: si no hay workspace la API no crea nada solo -- avisa
|
|
82
87
|
// (needsWorkspace) y aquí se le pide el nombre al usuario.
|
|
@@ -84,7 +89,7 @@ export async function runSetup() {
|
|
|
84
89
|
const res = await fetch(`${apiBase}/v1/bootstrap`, {
|
|
85
90
|
method: 'POST',
|
|
86
91
|
headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
|
|
87
|
-
body: JSON.stringify({ agentLabel, askIfMissing: true, ...extra }),
|
|
92
|
+
body: JSON.stringify({ agentLabel, machineId: machine.id, machineName: machine.name, askIfMissing: true, ...extra }),
|
|
88
93
|
})
|
|
89
94
|
const body = await res.json()
|
|
90
95
|
if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
|
|
@@ -123,6 +128,14 @@ export async function runSetup() {
|
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
const { workspaceId, token, projects } = body
|
|
131
|
+
// La API anterior a ORQ-415 no manda `agent`.
|
|
132
|
+
// El token anterior ya quedó revocado en el servidor: se reemplaza en todas
|
|
133
|
+
// las carpetas de esta herramienta AHORA, antes de las preguntas, para que
|
|
134
|
+
// cancelar el setup a la mitad no deje carpetas con un token muerto.
|
|
135
|
+
const renewedFolders = body.agent?.known ? replaceAgentToken({ workspaceId, agent, apiToken: token }) : []
|
|
136
|
+
if (body.agent?.known) console.log(`\nYa te conocía: "${body.agent.name}". Renové su token (el anterior quedó revocado).`)
|
|
137
|
+
else if (body.agent?.adopted) console.log(`\nTu agente "${agentLabel}" de antes quedó ligado a esta máquina como "${body.agent.name}".`)
|
|
138
|
+
else if (body.agent) console.log(`\nNuevo agente en tu equipo de Orquestra: "${body.agent.name}".`)
|
|
126
139
|
// La API de producción anterior a ORQ-388 no manda workspaceName.
|
|
127
140
|
const wsLabel = body.workspaceName ? ` del workspace "${body.workspaceName}"` : ''
|
|
128
141
|
let defaultProjectId
|
|
@@ -173,20 +186,64 @@ export async function runSetup() {
|
|
|
173
186
|
: folderAnswer.trim()
|
|
174
187
|
const folderPath = expanded ? resolve(expanded) : process.cwd()
|
|
175
188
|
|
|
176
|
-
upsertProjectMapping({ apiBase, apiToken: token, workspaceId, folderPath, projectId: defaultProjectId })
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
console.log('\
|
|
183
|
-
console.log('
|
|
184
|
-
console.log(` claude mcp add orquestra -- ${isWindows ? 'cmd /c ' : ''}npx -y orquestra-mcp@latest`)
|
|
185
|
-
if (isWindows) console.log(' (si "claude" no se reconoce: npm install -g @anthropic-ai/claude-code y abre otra terminal)')
|
|
186
|
-
console.log(' Después reinicia Claude Code o reconéctalo con /mcp.\n')
|
|
187
|
-
console.log(` Claude Desktop -- agrega esto a ${isWindows ? '%APPDATA%\\Claude\\' : '~/Library/Application Support/Claude/'}claude_desktop_config.json:`)
|
|
188
|
-
console.log(' { "mcpServers": { "orquestra": { "command": "npx", "args": ["-y", "orquestra-mcp@latest"] } } }\n')
|
|
189
|
-
console.log(' ¿Usas Cursor, ChatGPT/Codex o Gemini CLI? La config de cada uno está en:')
|
|
190
|
-
console.log(' https://www.orquestra.me/mcp#instalacion\n')
|
|
189
|
+
upsertProjectMapping({ apiBase, apiToken: token, workspaceId, folderPath, projectId: defaultProjectId, agent })
|
|
190
|
+
const others = renewedFolders.filter(f => f !== folderPath)
|
|
191
|
+
console.log(`\nListo. "${folderPath}" (y sus subcarpetas) ya apunta a este proyecto con ${agentLabel} -- guardado en ${CONFIG_PATH}.`)
|
|
192
|
+
if (others.length) console.log(`También actualicé el token de ${agentLabel} en: ${others.join(', ')}.`)
|
|
193
|
+
console.log('\nSiguiente paso -- conéctalo a tu herramienta (si ya estaba conectada, solo reconecta el MCP):\n')
|
|
194
|
+
console.log(connectionHelp(agent, agentLabel))
|
|
195
|
+
console.log('\n¿Otra herramienta en esta misma carpeta? Corre de nuevo "npx orquestra-mcp@latest setup" y elígela:')
|
|
196
|
+
console.log('cada una firma con su propio agente.')
|
|
191
197
|
console.log('Docs completas y todas las tools disponibles: https://www.orquestra.me/mcp')
|
|
192
198
|
}
|
|
199
|
+
|
|
200
|
+
// Sin preguntar "¿cómo se llama?" cada vez: se elige de la lista (o viene con
|
|
201
|
+
// --agent) y se marca lo que ya está conectado en esta máquina.
|
|
202
|
+
async function chooseTool(preset, machine) {
|
|
203
|
+
if (preset?.trim()) return TOOLS.find(t => agentKey(t) === agentKey(preset)) || preset.trim()
|
|
204
|
+
const connected = connectedAgents()
|
|
205
|
+
const already = TOOLS.filter(t => connected.has(agentKey(t)))
|
|
206
|
+
console.log(`\nEsta máquina: ${machine.name}${already.length ? ` (ya conectadas: ${already.join(', ')})` : ''}`)
|
|
207
|
+
console.log('¿Para qué herramienta es esta conexión?')
|
|
208
|
+
TOOLS.forEach((t, i) => console.log(` ${i + 1}. ${t}${connected.has(agentKey(t)) ? ' (ya conectada: renueva su token)' : ''}`))
|
|
209
|
+
console.log(` ${TOOLS.length + 1}. Otra`)
|
|
210
|
+
for (;;) {
|
|
211
|
+
const answer = (await prompt('Número [Enter para Claude Code]: ')).trim()
|
|
212
|
+
const idx = answer === '' ? 1 : parseInt(answer, 10)
|
|
213
|
+
if (idx >= 1 && idx <= TOOLS.length) return TOOLS[idx - 1]
|
|
214
|
+
if (idx === TOOLS.length + 1) {
|
|
215
|
+
const name = (await prompt('Nombre de la herramienta: ')).trim()
|
|
216
|
+
if (name) return name
|
|
217
|
+
}
|
|
218
|
+
console.log('Selección inválida.')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// La config de cada herramienta lleva ORQUESTRA_AGENT para que use SU token
|
|
223
|
+
// (sin eso usaría el default de la carpeta y firmaría como otra).
|
|
224
|
+
// En Windows Claude Code necesita `cmd /c` para lanzar npx, y el config de
|
|
225
|
+
// Claude Desktop vive en %APPDATA%. `@latest` evita que npx reuse una
|
|
226
|
+
// versión vieja de su caché.
|
|
227
|
+
function connectionHelp(agent, agentLabel) {
|
|
228
|
+
const isWindows = process.platform === 'win32'
|
|
229
|
+
const json = file => ` ${agentLabel} -- agrega esto a ${file}:\n` +
|
|
230
|
+
` { "mcpServers": { "orquestra": { "command": "npx", "args": ["-y", "orquestra-mcp@latest"], "env": { "ORQUESTRA_AGENT": "${agent}" } } } }`
|
|
231
|
+
if (agent === 'claude-code') {
|
|
232
|
+
return ' Claude Code:\n' +
|
|
233
|
+
` claude mcp add orquestra -e ORQUESTRA_AGENT=${agent} -- ${isWindows ? 'cmd /c ' : ''}npx -y orquestra-mcp@latest\n` +
|
|
234
|
+
(isWindows ? ' (si "claude" no se reconoce: npm install -g @anthropic-ai/claude-code y abre otra terminal)\n' : '') +
|
|
235
|
+
' (si ya estaba agregado: claude mcp remove orquestra y vuelve a agregarlo)\n' +
|
|
236
|
+
' Después reinicia Claude Code o reconéctalo con /mcp.'
|
|
237
|
+
}
|
|
238
|
+
if (agent === 'codex') {
|
|
239
|
+
return ' Codex -- en ~/.codex/config.toml:\n' +
|
|
240
|
+
' [mcp_servers.orquestra]\n' +
|
|
241
|
+
' command = "npx"\n' +
|
|
242
|
+
' args = ["-y", "orquestra-mcp@latest"]\n' +
|
|
243
|
+
` env = { ORQUESTRA_AGENT = "${agent}" }`
|
|
244
|
+
}
|
|
245
|
+
if (agent === 'cursor') return json('~/.cursor/mcp.json')
|
|
246
|
+
if (agent === 'gemini-cli') return json('~/.gemini/settings.json')
|
|
247
|
+
if (agent === 'claude-desktop') return json(`${isWindows ? '%APPDATA%\\Claude\\' : '~/Library/Application Support/Claude/'}claude_desktop_config.json`)
|
|
248
|
+
return json('la config de MCP de tu herramienta') + '\n Guía por herramienta: https://www.orquestra.me/mcp#instalacion'
|
|
249
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
// Aviso de versión nueva (ORQ-415): al arrancar, el server pregunta a npm cuál
|
|
6
|
+
// es la última versión publicada (caché de 12 h en ~/.orquestra, así casi
|
|
7
|
+
// ningún arranque sale a la red) y get_project_context la devuelve en
|
|
8
|
+
// `orquestraMcpUpdate` para que el agente le diga al usuario cómo actualizar.
|
|
9
|
+
// Nunca bloquea el arranque ni falla: sin red simplemente no hay aviso.
|
|
10
|
+
const CACHE_PATH = join(homedir(), '.orquestra', 'update-check.json')
|
|
11
|
+
const CACHE_TTL_MS = 12 * 60 * 60 * 1000
|
|
12
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/orquestra-mcp/latest'
|
|
13
|
+
|
|
14
|
+
let latestKnown = null
|
|
15
|
+
|
|
16
|
+
export function isNewer(latest, current) {
|
|
17
|
+
const parts = v => String(v).split('-')[0].split('.').map(n => parseInt(n, 10) || 0)
|
|
18
|
+
const [a, b] = [parts(latest), parts(current)]
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0)
|
|
21
|
+
}
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function startUpdateCheck({ fetchImpl = fetch, now = Date.now() } = {}) {
|
|
26
|
+
if (process.env.ORQUESTRA_NO_UPDATE_CHECK) return null
|
|
27
|
+
try {
|
|
28
|
+
const cached = JSON.parse(readFileSync(CACHE_PATH, 'utf8'))
|
|
29
|
+
if (cached.latest && now - cached.checkedAt < CACHE_TTL_MS) return (latestKnown = cached.latest)
|
|
30
|
+
} catch {}
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(3000) })
|
|
33
|
+
if (!res.ok) return null
|
|
34
|
+
const { version } = await res.json()
|
|
35
|
+
if (!version) return null
|
|
36
|
+
latestKnown = version
|
|
37
|
+
mkdirSync(dirname(CACHE_PATH), { recursive: true })
|
|
38
|
+
writeFileSync(CACHE_PATH, JSON.stringify({ latest: version, checkedAt: now }))
|
|
39
|
+
return version
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function updateNotice(current) {
|
|
46
|
+
if (!latestKnown || !isNewer(latestKnown, current)) return null
|
|
47
|
+
return {
|
|
48
|
+
current,
|
|
49
|
+
latest: latestKnown,
|
|
50
|
+
message:
|
|
51
|
+
`Hay una versión nueva de orquestra-mcp (${latestKnown}; esta sesión usa ${current}). ` +
|
|
52
|
+
`Para actualizar, reinicia tu herramienta o reconecta el MCP (en Claude Code: /mcp → orquestra → Reconnect). ` +
|
|
53
|
+
`Si sigue la versión vieja, en la config del MCP cambia "orquestra-mcp" por "orquestra-mcp@latest" ` +
|
|
54
|
+
`(sin @latest, npx reusa la versión que tiene en caché). Novedades: https://www.orquestra.me/mcp`,
|
|
55
|
+
}
|
|
56
|
+
}
|