orquestra-mcp 1.49.0 → 1.51.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.
@@ -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
- await runSetup()
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.49.0",
3
+ "version": "1.51.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 folderConfig = resolveProjectForCwd(fileConfig?.projects, process.cwd())
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
@@ -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
- export function upsertProjectMapping({ apiBase, apiToken, workspaceId, folderPath, projectId }) {
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
- projects[folderPath] = { apiBase, apiToken, workspaceId, projectId }
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'
@@ -25,6 +26,8 @@ function textResult(data) {
25
26
  }
26
27
 
27
28
  const PROJECT_ID_DESC = 'ID del proyecto dentro del workspace configurado. Si hay un proyecto default configurado (via "setup" o ORQUESTRA_DEFAULT_PROJECT_ID), este argumento se ignora -- el server queda fijo a ese proyecto y no puede leer/escribir otro.'
29
+ // ORQ-411: los textos largos de idea, riesgo y módulo también cuentan para [[...]] y "Mencionado en".
30
+ const WIKILINK_DESC = 'Admite [[Título]], [[Título|texto]] o [[ORQ-123]] para enlazar otra nota, doc, tarea, reunión, idea, riesgo, módulo, contenido o distribución del proyecto.'
28
31
 
29
32
  const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'design', 'security', 'meeting', 'call', 'followup', 'business']
30
33
  // Actividades (ORQ-295): reunión/llamada/seguimiento. 'business' sigue siendo tarea (entregable). Mismas listas que
@@ -102,7 +105,7 @@ const SETUP_GUIDE = {
102
105
  // preguntar en vez de adivinar.
103
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.
104
107
 
105
- 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.
106
109
 
107
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:
108
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.
@@ -173,7 +176,10 @@ export function createSetupNeededServer(reason) {
173
176
  server.registerTool('diagnostico_conexion', {
174
177
  description: 'Explica por qué Orquestra no está conectado en esta carpeta y cómo arreglarlo.',
175
178
  inputSchema: {},
176
- }, async () => ({ content: [{ type: 'text', text: `orquestra-mcp ${PKG_VERSION}: ${reason}` }] }))
179
+ }, async () => {
180
+ const update = updateNotice(PKG_VERSION)
181
+ return { content: [{ type: 'text', text: `orquestra-mcp ${PKG_VERSION}: ${reason}${update ? `\n\n${update.message}` : ''}` }] }
182
+ })
177
183
  return server
178
184
  }
179
185
 
@@ -203,14 +209,14 @@ export function createServer() {
203
209
  return projectId
204
210
  }
205
211
 
206
- function registerRead(name, path, description, extraSchema, buildParams) {
212
+ function registerRead(name, path, description, extraSchema, buildParams, decorate = data => data) {
207
213
  server.registerTool(
208
214
  name,
209
215
  { description, inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC), ...extraSchema } },
210
216
  async (args) => {
211
217
  try {
212
218
  const data = await apiGet(config, path, buildParams(args, resolveProjectId(args.projectId)))
213
- return textResult(data)
219
+ return textResult(decorate(data))
214
220
  } catch (err) {
215
221
  return errorResult(err)
216
222
  }
@@ -262,7 +268,11 @@ export function createServer() {
262
268
  '/v1/context',
263
269
  'Resumen compacto del estado de un proyecto de Orquestra: fase, plataformas, tareas abiertas/vencidas/bloqueadas, módulos, ideas y changelog reciente.',
264
270
  {},
265
- (args, projectId) => ({ projectId })
271
+ (args, projectId) => ({ projectId }),
272
+ data => {
273
+ const update = updateNotice(PKG_VERSION)
274
+ return update ? { ...data, orquestraMcpUpdate: update } : data
275
+ }
266
276
  )
267
277
 
268
278
  registerRead(
@@ -293,10 +303,10 @@ export function createServer() {
293
303
  registerRead(
294
304
  'search',
295
305
  '/v1/search',
296
- 'Busca por texto en tareas, actividades (reuniones/llamadas), notas, ideas y docs de un proyecto de Orquestra. 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.',
306
+ '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.',
297
307
  {
298
308
  q: z.string().describe('Texto a buscar'),
299
- 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.'),
309
+ 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.'),
300
310
  },
301
311
  (args, projectId) => ({ projectId, q: args.q, types: args.types?.join(',') })
302
312
  )
@@ -1008,10 +1018,10 @@ export function createServer() {
1008
1018
  registerRead(
1009
1019
  'get_backlinks',
1010
1020
  '/v1/backlinks',
1011
- 'Backlinks ("Mencionado en"): qué notas, docs, tareas e ideas enlazan con [[...]] a la entidad pedida. Útil antes de editar o cerrar algo para ver qué depende de eso.',
1021
+ 'Backlinks ("Mencionado en"): qué notas, docs, tareas, reuniones, ideas, riesgos y módulos enlazan con [[...]] a la entidad pedida (también cuentan los puntos a revisar de una reunión y las instrucciones de las verificaciones). Útil antes de editar o cerrar algo para ver qué depende de eso.',
1012
1022
  {
1013
- type: z.enum(['note', 'doc', 'task', 'idea']),
1014
- id: z.string().describe('id de la nota/doc/tarea/idea'),
1023
+ type: z.enum(['note', 'doc', 'task', 'activity', 'idea', 'risk', 'module', 'content', 'distribution']).describe('"activity" para reunión/llamada/seguimiento'),
1024
+ id: z.string().describe('id del ítem'),
1015
1025
  },
1016
1026
  (args, projectId) => ({ projectId, type: args.type, id: args.id })
1017
1027
  )
@@ -1993,7 +2003,7 @@ export function createServer() {
1993
2003
  {
1994
2004
  relatedTo: RELATED_TO,
1995
2005
  title: z.string().describe('Título de la idea'),
1996
- description: z.string().optional(),
2006
+ description: z.string().optional().describe(WIKILINK_DESC),
1997
2007
  priority: z.enum(TASK_PRIORITIES).optional().describe('Default: medium -- se puede derivar de impact+effort si los mandas (alto+bajo→high, alto+alto o bajo+bajo→medium, bajo+alto→low).'),
1998
2008
  categoryId: z.string().optional(),
1999
2009
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
@@ -2014,7 +2024,7 @@ export function createServer() {
2014
2024
 
2015
2025
  registerUpdate('update_idea', '/v1/ideas', 'Edita una idea existente. Todos los campos son opcionales -- solo se cambian los que mandes.', {
2016
2026
  title: z.string().optional(),
2017
- description: z.string().optional(),
2027
+ description: z.string().optional().describe(WIKILINK_DESC),
2018
2028
  priority: z.enum(TASK_PRIORITIES).optional(),
2019
2029
  categoryId: z.string().optional(),
2020
2030
  dueDate: z.string().optional().describe('YYYY-MM-DD'),
@@ -2236,14 +2246,19 @@ export function createServer() {
2236
2246
  'Lista TODAS las notas de un proyecto de Orquestra, con todos sus campos -- a diferencia de get_project_context/search (resumen recortado), útil para auditar o encontrar el id exacto antes de update_note.',
2237
2247
  {}, (args, projectId) => ({ projectId }))
2238
2248
 
2249
+ // ORQ-414: puntos de la idea, mismo shape que agendaItems de las actividades.
2250
+ const NOTE_ITEMS = z.array(z.union([z.string(), z.object({ id: z.string().optional(), text: z.string() })])).optional()
2251
+ .describe('Puntos de la idea, debajo de la descripción (body). Lista simple, NO checklist (sin palomitas). Reemplaza la lista COMPLETA: para agregar uno, lee la actual con list_notes y manda todos (con sus id para conservarlos).')
2252
+
2239
2253
  registerCreate(
2240
2254
  'create_note',
2241
2255
  '/v1/notes',
2242
- 'Crea una nota en un proyecto de Orquestra. En el texto, [[Título]] o [[Título|texto]] enlaza otra nota, doc, tarea o idea del proyecto (o [[ORQ-123]] por folio) -- úsalo para conectar el conocimiento en vez de pegar ids.',
2256
+ 'Crea una nota en un proyecto de Orquestra. En el texto, [[Título]] o [[Título|texto]] enlaza otra nota, doc, tarea, reunión, idea, riesgo, módulo, contenido o distribución del proyecto (o [[ORQ-123]] por folio) -- úsalo para conectar el conocimiento en vez de pegar ids.',
2243
2257
  {
2244
2258
  relatedTo: RELATED_TO,
2245
2259
  title: z.string().describe('Título de la nota'),
2246
- body: z.string().optional(),
2260
+ body: z.string().optional().describe('Descripción de la nota.'),
2261
+ noteItems: NOTE_ITEMS,
2247
2262
  pinned: z.boolean().optional(),
2248
2263
  moduleId: z.string().optional().describe('Módulo relacionado, si la nota es sobre uno específico -- ligarla ayuda a encontrarla después (ver list_modules)'),
2249
2264
  taskId: z.string().optional().describe('Tarea relacionada, si la nota es sobre una específica (ver list_tasks)'),
@@ -2265,6 +2280,7 @@ export function createServer() {
2265
2280
  {
2266
2281
  title: z.string().optional(),
2267
2282
  body: z.string().optional(),
2283
+ noteItems: NOTE_ITEMS,
2268
2284
  pinned: z.boolean().optional(),
2269
2285
  moduleId: z.string().optional(),
2270
2286
  taskId: z.string().optional(),
@@ -2315,7 +2331,7 @@ export function createServer() {
2315
2331
  registerCreate(
2316
2332
  'create_doc',
2317
2333
  '/v1/docs',
2318
- 'Crea un documento en un proyecto de Orquestra. En el texto, [[Título]] o [[Título|texto]] enlaza otra nota, doc, tarea o idea del proyecto (o [[ORQ-123]] por folio) -- úsalo para conectar el conocimiento en vez de pegar ids.',
2334
+ 'Crea un documento en un proyecto de Orquestra. En el texto, [[Título]] o [[Título|texto]] enlaza otra nota, doc, tarea, reunión, idea, riesgo, módulo, contenido o distribución del proyecto (o [[ORQ-123]] por folio) -- úsalo para conectar el conocimiento en vez de pegar ids.',
2319
2335
  {
2320
2336
  relatedTo: RELATED_TO,
2321
2337
  type: z.enum(DOC_TYPES).optional().describe('Tipo de documento -- obligatorio salvo que mandes templateId (la plantilla trae su tipo)'),
@@ -2352,7 +2368,7 @@ export function createServer() {
2352
2368
  'Edita un documento existente. Solo se cambian los campos que mandes; content REEMPLAZA el texto entero (lee primero con get_doc y manda el documento completo, no solo el cambio). La versión anterior queda en el historial del doc ("Versiones"). Si cambias el título, los [[Título viejo]] de todo el proyecto se reescriben solos.',
2353
2369
  {
2354
2370
  title: z.string().optional(),
2355
- content: z.string().optional().describe('Markdown completo del doc. [[Título]] enlaza otra nota, doc, tarea o idea.'),
2371
+ content: z.string().optional().describe('Markdown completo del doc. [[Título]] enlaza otra nota, doc, tarea, reunión, idea, riesgo, módulo, contenido o distribución.'),
2356
2372
  type: z.enum(DOC_TYPES).optional(),
2357
2373
  adrStatus: z.enum(ADR_STATUSES).optional().describe('Solo para type=\'adr\'.'),
2358
2374
  adrContext: z.string().optional().describe('Solo para type=\'adr\'.'),
@@ -2372,7 +2388,7 @@ export function createServer() {
2372
2388
  const MODULE_KEY = z.string().optional().describe('Clave del módulo (ej. "AUTH", "PAGOS"): 2-10 letras/números, empieza con letra, única en el proyecto. Numera sus tareas (AUTH-1, AUTH-2...) en moduleCode, además del folio ORQ-N. Solo si el usuario la pidió o el proyecto ya usa claves -- no la inventes.')
2373
2389
  registerCreate('create_module', '/v1/modules', 'Crea un módulo en un proyecto de Orquestra.', {
2374
2390
  name: z.string().describe('Nombre del módulo'),
2375
- description: z.string().optional(),
2391
+ description: z.string().optional().describe(WIKILINK_DESC),
2376
2392
  statusId: z.string().optional(),
2377
2393
  categoryId: z.string().optional(),
2378
2394
  scope: SCOPE_SCHEMA.describe('Alcance por distribución, ej: [{distId, layer, done}]'),
@@ -2381,12 +2397,12 @@ export function createServer() {
2381
2397
  shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
2382
2398
  ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
2383
2399
  dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
2384
- closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
2400
+ closureCriteria: z.string().optional().describe(`Texto libre: cuándo se considera verdaderamente cerrado. ${WIKILINK_DESC}`),
2385
2401
  key: MODULE_KEY,
2386
2402
  })
2387
2403
  registerUpdate('update_module', '/v1/modules', 'Edita un módulo existente (incluido su scope multiplataforma).', {
2388
2404
  name: z.string().optional(),
2389
- description: z.string().optional(),
2405
+ description: z.string().optional().describe(WIKILINK_DESC),
2390
2406
  statusId: z.string().optional(),
2391
2407
  categoryId: z.string().optional(),
2392
2408
  scope: SCOPE_SCHEMA,
@@ -2395,7 +2411,7 @@ export function createServer() {
2395
2411
  shippedVersion: z.string().optional().describe('Versión del changelog en la que se envió, ej: "1.4.0"'),
2396
2412
  ownerUid: z.string().optional().describe('uid del miembro responsable del avance del módulo'),
2397
2413
  dependsOnModuleIds: z.array(z.string()).optional().describe('IDs de módulos de los que este depende (dependencia planeada, manual)'),
2398
- closureCriteria: z.string().optional().describe('Texto libre: cuándo se considera verdaderamente cerrado'),
2414
+ closureCriteria: z.string().optional().describe(`Texto libre: cuándo se considera verdaderamente cerrado. ${WIKILINK_DESC}`),
2399
2415
  key: MODULE_KEY.describe(`${MODULE_KEY.description} Poner o cambiar la clave numera las tareas del módulo que todavía no tienen una (por orden de creación; la respuesta trae assignedCodes). "" la quita: las tareas conservan la suya.`),
2400
2416
  })
2401
2417
 
@@ -2678,10 +2694,10 @@ export function createServer() {
2678
2694
  registerCreate('create_risk', '/v1/risks', 'Registra un riesgo/bloqueo a nivel de proyecto -- algo que puede hacer fallar o atrasar el proyecto en general, no el bloqueo puntual de una tarea (para eso usa relatedTaskId en update_task).', {
2679
2695
  relatedTo: RELATED_TO,
2680
2696
  title: z.string(),
2681
- description: z.string().optional(),
2697
+ description: z.string().optional().describe(WIKILINK_DESC),
2682
2698
  probability: z.enum(RISK_LEVELS).optional().describe('Default: medium'),
2683
2699
  impact: z.enum(RISK_LEVELS).optional().describe('Default: medium'),
2684
- mitigation: z.string().optional().describe('Plan o texto libre de qué se hace para reducir este riesgo'),
2700
+ mitigation: z.string().optional().describe(`Plan o texto libre de qué se hace para reducir este riesgo. ${WIKILINK_DESC}`),
2685
2701
  status: z.enum(RISK_STATUSES).optional().describe('Default: open. "closed" en vez de borrar -- nunca se borra un riesgo.'),
2686
2702
  ownerUid: z.string().optional().describe('Dueño del riesgo -- le avisa solo a esa persona al asignarla (ver list de miembros del workspace)'),
2687
2703
  moduleId: z.string().optional().describe('Módulo relacionado, si aplica (ver list_modules)'),
@@ -2689,10 +2705,10 @@ export function createServer() {
2689
2705
 
2690
2706
  registerUpdate('update_risk', '/v1/risks', 'Edita un riesgo existente -- típicamente para actualizar su mitigación o cerrarlo (status: "closed") cuando deja de aplicar.', {
2691
2707
  title: z.string().optional(),
2692
- description: z.string().optional(),
2708
+ description: z.string().optional().describe(WIKILINK_DESC),
2693
2709
  probability: z.enum(RISK_LEVELS).optional(),
2694
2710
  impact: z.enum(RISK_LEVELS).optional(),
2695
- mitigation: z.string().optional(),
2711
+ mitigation: z.string().optional().describe(WIKILINK_DESC),
2696
2712
  status: z.enum(RISK_STATUSES).optional(),
2697
2713
  ownerUid: z.string().optional(),
2698
2714
  moduleId: z.string().optional(),
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 agentLabelAnswer = await prompt('¿Cómo se llama este agente/herramienta? [Claude Code]: ')
79
- const agentLabel = agentLabelAnswer.trim() || 'Claude Code'
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
- console.log(`\nListo. "${folderPath}" (y sus subcarpetas) ya apunta a este proyecto -- guardado en ${CONFIG_PATH}.`)
178
- // En Windows Claude Code necesita `cmd /c` para lanzar npx, y el config de
179
- // Claude Desktop vive en %APPDATA%. `@latest` evita que npx reuse una
180
- // versión vieja de su caché.
181
- const isWindows = process.platform === 'win32'
182
- console.log('\nSiguiente paso -- conéctalo a tu agente de IA:\n')
183
- console.log(' Claude Code:')
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
+ }