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.
- 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 +40 -24
- 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.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
|
|
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'
|
|
@@ -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 () =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
+
}
|