orquestra-mcp 1.44.0 → 1.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,25 +12,34 @@ configurar/sincronizar un proyecto completo sin crear duplicados.
12
12
  Ver `CLAUDE.md` para contexto de desarrollo. Ver la memoria del proyecto
13
13
  `orquestra-mcp-api-plan` para las 8 fases del plan MCP/API completo.
14
14
 
15
- ## Setup rápido (recomendado)
15
+ ## Empezar (macOS, Linux y Windows)
16
+
17
+ Necesitas una cuenta en [app.orquestra.me](https://app.orquestra.me) y Node 18+.
18
+
19
+ **1. Conecta tu cuenta y tu proyecto** — desde la carpeta de tu proyecto:
16
20
 
17
21
  ```bash
18
- npm install
19
- node bin/orquestra-mcp.mjs setup
22
+ npx orquestra-mcp@latest setup
20
23
  ```
21
24
 
22
- Esto abre tu navegador en `https://app.orquestra.me/cli-auth` para
23
- confirmar tu sesión (Google o email/password, lo que ya uses) y:
25
+ Abre el navegador para confirmar tu sesión (Google o email/password) y luego:
26
+
27
+ - Si **no tienes workspace**, te pide su nombre y después el de tu primer proyecto.
28
+ - Si **perteneces a varios workspaces**, te deja elegir en cuál trabajar (elegir aquí
29
+ no cambia el que tienes abierto en la web).
30
+ - Eliges un proyecto de ese workspace o creas uno nuevo.
31
+
32
+ La conexión se guarda **por carpeta** en `~/.orquestra/mcp-config.json`: puedes tener
33
+ distintos proyectos (o cuentas) en distintas carpetas. Para conectar otra carpeta o
34
+ cambiar de proyecto, vuelve a correr el setup ahí.
24
35
 
25
- - Si **no tienes** workspace/proyecto todavía: te crea uno default
26
- ("Mi primer proyecto", con los 6 estatus base) — puedes renombrarlo o
27
- cambiarlo cuando quieras desde `orquestra-web`.
28
- - Si **ya tienes** uno o varios: te deja elegir cuál usar por default
29
- desde la misma terminal.
36
+ **2. Regístralo en tu agente** (ver abajo: Claude Code, Claude Desktop, Cursor, Codex,
37
+ Gemini CLI). Usa `orquestra-mcp@latest` para que `npx` no se quede con una versión vieja
38
+ de su caché.
30
39
 
31
- Guarda todo (`apiToken`, `workspaceId`, `defaultProjectId`) en
32
- `~/.orquestra/mcp-config.json` — después de esto, `node bin/orquestra-mcp.mjs`
33
- (sin más flags ni env vars) ya funciona.
40
+ **Ver la versión / actualizar:** `npm view orquestra-mcp version` muestra la última
41
+ publicada. Con `@latest` cada arranque la usa; el servidor que ya está corriendo conserva
42
+ la suya hasta que reconectas (en Claude Code: `/mcp` → reconectar).
34
43
 
35
44
  Variable opcional `ORQUESTRA_WEB_URL` para apuntar el login a otro lugar
36
45
  (ej. `http://localhost:5173` mientras desarrollas `orquestra-web` local en
@@ -71,35 +80,57 @@ Variables de entorno (todas opcionales si ya corriste `setup` — se leen de
71
80
 
72
81
  ### Claude Code
73
82
 
74
- Si ya corriste `setup`, alcanza con:
83
+ macOS / Linux:
75
84
 
76
85
  ```bash
77
- claude mcp add orquestra -- node /ruta/absoluta/a/orquestra-mcp/bin/orquestra-mcp.mjs
86
+ claude mcp add orquestra -- npx -y orquestra-mcp@latest
78
87
  ```
79
88
 
80
- O manual, sin `setup`:
89
+ Windows (PowerShell) — `npx` necesita `cmd /c` delante:
81
90
 
82
- ```bash
83
- claude mcp add orquestra \
84
- -e ORQUESTRA_API_TOKEN=orq_... \
85
- -e ORQUESTRA_WORKSPACE_ID=... \
86
- -- node /ruta/absoluta/a/orquestra-mcp/bin/orquestra-mcp.mjs
91
+ ```powershell
92
+ claude mcp add orquestra -- cmd /c npx -y orquestra-mcp@latest
87
93
  ```
88
94
 
95
+ Agrega `--scope user` después de `add` para tenerlo en todos tus proyectos. Si PowerShell
96
+ dice que `claude` no se reconoce, falta instalar Claude Code:
97
+ `npm install -g @anthropic-ai/claude-code` (y vuelve a abrir la terminal). Si ya lo habías
98
+ agregado sin `@latest`, primero `claude mcp remove orquestra`.
99
+
89
100
  ### Claude Desktop (`claude_desktop_config.json`)
90
101
 
102
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
103
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
104
+
91
105
  ```json
92
106
  {
93
107
  "mcpServers": {
94
108
  "orquestra": {
95
- "command": "node",
96
- "args": ["/ruta/absoluta/a/orquestra-mcp/bin/orquestra-mcp.mjs"]
109
+ "command": "npx",
110
+ "args": ["-y", "orquestra-mcp@latest"]
97
111
  }
98
112
  }
99
113
  }
100
114
  ```
101
115
 
102
- (sin `env` si ya corriste `setup`; si no, agrega `ORQUESTRA_API_TOKEN`/`ORQUESTRA_WORKSPACE_ID` igual que en Claude Code.)
116
+ Cierra la app por completo (en Windows también desde el ícono junto al reloj) y vuelve a
117
+ abrirla.
118
+
119
+ ### Cursor, Codex CLI, Gemini CLI
120
+
121
+ Mismo comando (`npx -y orquestra-mcp@latest`), cambia el archivo — ver
122
+ [orquestra.me/mcp#instalacion](https://www.orquestra.me/mcp#instalacion). En Windows, si
123
+ el agente no encuentra `npx`, usa `"command": "cmd"` con
124
+ `"args": ["/c", "npx", "-y", "orquestra-mcp@latest"]`.
125
+
126
+ ### Sin `setup` (variables de entorno)
127
+
128
+ ```bash
129
+ claude mcp add orquestra \
130
+ -e ORQUESTRA_API_TOKEN=orq_... \
131
+ -e ORQUESTRA_WORKSPACE_ID=... \
132
+ -- npx -y orquestra-mcp@latest
133
+ ```
103
134
 
104
135
  ## Tools
105
136
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
- import { createServer } from '../src/server.mjs'
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
6
 
@@ -9,7 +9,7 @@ const HELP_TEXT = `orquestra-mcp -- Servidor MCP de Orquestra (132 tools)
9
9
  Uso:
10
10
  npx orquestra-mcp Inicia el servidor MCP por stdio -- lo usa tu cliente
11
11
  MCP (Claude Code, Claude Desktop, etc.), no se corre a mano
12
- npx orquestra-mcp setup Login por navegador + configura tu proyecto default
12
+ npx orquestra-mcp setup Login por navegador + conecta esta carpeta a un proyecto
13
13
  npx orquestra-mcp vault sync <carpeta> [--apply]
14
14
  Sincroniza la base de conocimiento del workspace con una
15
15
  carpeta de tu vault de Obsidian (la 1ra vez solo muestra el plan)
@@ -50,7 +50,7 @@ async function main() {
50
50
  server = createServer()
51
51
  } catch (err) {
52
52
  console.error(`[orquestra-mcp] ${err.message}`)
53
- process.exit(1)
53
+ server = createSetupNeededServer(err.message)
54
54
  }
55
55
 
56
56
  const transport = new StdioServerTransport()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.44.0",
3
+ "version": "1.46.1",
4
4
  "description": "Servidor MCP de Orquestra -- 135 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "engines": {
16
16
  "node": ">=18"
17
17
  },
18
- "homepage": "https://orquestra.me/mcp",
18
+ "homepage": "https://www.orquestra.me/mcp",
19
19
  "keywords": [
20
20
  "mcp",
21
21
  "model-context-protocol",
package/src/api.mjs CHANGED
@@ -27,12 +27,15 @@ export function loadConfig() {
27
27
  if (!apiToken) missing.push('ORQUESTRA_API_TOKEN')
28
28
  if (!workspaceId) missing.push('ORQUESTRA_WORKSPACE_ID')
29
29
  if (missing.length) {
30
+ const folders = Object.keys(fileConfig?.projects || {})
30
31
  throw new Error(
31
- `Faltan variables de entorno requeridas: ${missing.join(', ')} (y no hay config en ${CONFIG_PATH}). ` +
32
- `Corre "npx orquestra-mcp setup" para conectarlo con tu cuenta -- crea un workspace/proyecto ` +
33
- `default si no tienes uno, o te deja elegir uno existente. Alternativa manual: 1) cuenta+workspace ` +
34
- `ya creados en orquestra-web, 2) generar un token con POST ${apiBase}/v1/tokens con tu ID token ` +
35
- `de Firebase, y 3) pasar ese token + el workspaceId acá.`
32
+ `Esta carpeta (${process.cwd()}) no está conectada a Orquestra. ` +
33
+ (folders.length
34
+ ? `Carpetas conectadas en ${CONFIG_PATH}: ${folders.join(' | ')}. `
35
+ : `No hay ninguna carpeta conectada (${CONFIG_PATH} no existe o está vacío). `) +
36
+ `Solución: abre una terminal en esta carpeta y corre "npx orquestra-mcp@latest setup" ` +
37
+ `(o ábrela desde una carpeta que ya esté conectada), y después reconecta el MCP. ` +
38
+ `Alternativa sin setup: variables de entorno ${missing.join(' y ')}.`
36
39
  )
37
40
  }
38
41
 
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
2
2
  import { homedir } from 'os'
3
- import { join, dirname, sep } from 'path'
3
+ import { join, dirname, win32, posix } from 'path'
4
4
 
5
5
  export const CONFIG_PATH = join(homedir(), '.orquestra', 'mcp-config.json')
6
6
 
@@ -63,13 +63,27 @@ export function upsertProjectMapping({ apiBase, apiToken, workspaceId, folderPat
63
63
  // gana, así que un repo individual con su propio mapeo (y su propia cuenta)
64
64
  // puede sobreescribir el de la carpeta contenedora que lo engloba. Devuelve
65
65
  // el objeto {apiBase, apiToken, workspaceId, projectId} de esa carpeta.
66
- export function resolveProjectForCwd(projects, cwd) {
66
+ // En Windows las rutas no distinguen mayúsculas, y VS Code arranca el
67
+ // servidor con la unidad en minúscula ("c:\Users\...") aunque setup haya
68
+ // guardado "C:\Users\..." (o con "/" en vez de "\"). Comparar exacto hacía
69
+ // que no encontrara la carpeta y el servidor se cerrara al arrancar
70
+ // ("Connection closed" en Claude Code, 21 oct 2026). `platform` es para tests.
71
+ export function folderKey(folderPath, platform = process.platform) {
72
+ const isWin = platform === 'win32'
73
+ const resolved = (isWin ? win32 : posix).resolve(folderPath)
74
+ const trimmed = resolved.length > 1 && !/^[a-z]:\\$/i.test(resolved) ? resolved.replace(/[\\/]+$/, '') : resolved
75
+ return isWin ? trimmed.toLowerCase() : trimmed
76
+ }
77
+
78
+ export function resolveProjectForCwd(projects, cwd, platform = process.platform) {
67
79
  if (!projects) return undefined
80
+ const sep = platform === 'win32' ? '\\' : '/'
81
+ const target = folderKey(cwd, platform)
68
82
  let best = null
69
83
  for (const folderPath of Object.keys(projects)) {
70
- const normalized = folderPath.endsWith(sep) ? folderPath.slice(0, -1) : folderPath
71
- if (cwd !== normalized && !cwd.startsWith(normalized + sep)) continue
72
- if (!best || normalized.length > best.length) best = normalized
84
+ const key = folderKey(folderPath, platform)
85
+ if (target !== key && !target.startsWith(key.endsWith(sep) ? key : key + sep)) continue
86
+ if (!best || key.length > best.key.length) best = { key, folderPath }
73
87
  }
74
- return best ? projects[best] : undefined
88
+ return best ? projects[best.folderPath] : undefined
75
89
  }
@@ -526,7 +526,7 @@ export const PLATFORM_PLAYBOOKS = {
526
526
  {
527
527
  title: 'Medir y corregir Core Web Vitals en PageSpeed Insights (móvil)',
528
528
  description: 'Correr PageSpeed Insights sobre la home y las páginas clave. Objetivo: LCP < 2.5s, CLS < 0.1, INP < 200ms. Atacar primero imágenes sin comprimir, fuentes bloqueantes y JS sin dividir.',
529
- type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5,
529
+ type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5, checkKey: 'core_web_vitals',
530
530
  },
531
531
  {
532
532
  title: 'Fijar dominio canónico y redirecciones 301 (www / http → https)',
package/src/server.mjs CHANGED
@@ -161,6 +161,22 @@ const SCOPE_EVIDENCE = {
161
161
  notApplicableReason: z.string().optional().describe('Motivo de "no aplica" (obligatorio si notApplicable es true)'),
162
162
  }
163
163
 
164
+ // Sin conexión para la carpeta (loadConfig falló): antes el proceso se cerraba
165
+ // y el cliente solo mostraba "Connection closed", sin el motivo. Ahora arranca
166
+ // con una sola tool que explica qué pasa y cómo arreglarlo, para que el agente
167
+ // se lo diga al usuario.
168
+ export function createSetupNeededServer(reason) {
169
+ const server = new McpServer(
170
+ { name: 'orquestra-mcp', version: PKG_VERSION },
171
+ { instructions: `Orquestra no está conectado en esta carpeta, así que sus herramientas no están disponibles. Si el usuario pide algo de Orquestra, llama a diagnostico_conexion y explícale cómo arreglarlo. Motivo: ${reason}` },
172
+ )
173
+ server.registerTool('diagnostico_conexion', {
174
+ description: 'Explica por qué Orquestra no está conectado en esta carpeta y cómo arreglarlo.',
175
+ inputSchema: {},
176
+ }, async () => ({ content: [{ type: 'text', text: `orquestra-mcp ${PKG_VERSION}: ${reason}` }] }))
177
+ return server
178
+ }
179
+
164
180
  export function createServer() {
165
181
  const config = loadConfig()
166
182
  const server = new McpServer({ name: 'orquestra-mcp', version: PKG_VERSION }, { instructions: SERVER_INSTRUCTIONS })
@@ -337,6 +353,26 @@ export function createServer() {
337
353
  {}
338
354
  )
339
355
 
356
+ // PageSpeed Insights por distribución web (ORQ-377). La última medición
357
+ // también viene en list_distributions (campo `pagespeed` de cada una).
358
+ registerRead(
359
+ 'get_pagespeed',
360
+ '/v1/pagespeed',
361
+ 'Historial de mediciones de PageSpeed Insights (móvil) de una distribución web, más reciente primero: scores de Performance/Accesibilidad/Buenas prácticas/SEO, métricas de laboratorio (LCP, CLS, TBT), Core Web Vitals de usuarios reales (CrUX: LCP, INP, CLS con categoría FAST/AVERAGE/SLOW; null si el sitio no tiene tráfico suficiente), oportunidades con ahorro estimado y auditorías que fallan. Se mide solo cada lunes; para una medición nueva usa measure_pagespeed.',
362
+ {
363
+ distributionId: z.string().describe('Distribución type="web" con domain configurado (ver list_distributions)'),
364
+ limit: z.number().int().min(1).max(100).optional().describe('Cuántas mediciones traer (default 20)'),
365
+ },
366
+ (args, projectId) => ({ projectId, distributionId: args.distributionId, limit: args.limit })
367
+ )
368
+
369
+ registerCreate(
370
+ 'measure_pagespeed',
371
+ '/v1/pagespeed',
372
+ 'Pide una medición nueva de PageSpeed Insights (móvil) para una distribución web con dominio. Corre en segundo plano (10-40 s): responde de inmediato con status "running"; el resultado aparece después en el campo `pagespeed` de la distribución (list_distributions) y en get_pagespeed. 409 si ya hay una en curso.',
373
+ { distributionId: z.string().describe('Distribución type="web" con domain configurado') }
374
+ )
375
+
340
376
  registerRead(
341
377
  'get_infra_status',
342
378
  '/v1/infra',
@@ -2637,12 +2673,12 @@ export function createServer() {
2637
2673
 
2638
2674
  registerRead('list_agents', '/v1/agents',
2639
2675
  'Lista los agentes (kind+máquina+stack+ruta local) configurados en un proyecto. Úsala antes de create_agent o de create_agent_run con stack, para saber si ya existe uno.',
2640
- { kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
2676
+ { kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse: performance/accesibilidad/buenas prácticas/SEO + LCP/CLS/TBT) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
2641
2677
  (args, projectId) => ({ projectId, ...(args.kind ? { kind: args.kind } : {}) }))
2642
2678
 
2643
2679
  registerCreate('create_agent', '/v1/agents', 'Crea un agente: asocia un kind ("qa" | "security" | "performance" | "screenshots") a una máquina (runner, ver Workspace → Máquinas), un stack de este proyecto y una ruta de checkout en ESA máquina. kind="performance" además necesita perfUrl.', {
2644
2680
  name: z.string().describe('Nombre a mostrar, ej. "Agente web"'),
2645
- kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
2681
+ kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse: performance/accesibilidad/buenas prácticas/SEO + LCP/CLS/TBT) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
2646
2682
  runnerId: z.string().describe('ID de la máquina emparejada -- ver list_agents o Workspace → Máquinas'),
2647
2683
  stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los 4 kinds. security/performance solo tienen lógica real para "web" hoy; screenshots solo para "ios"/"android"'),
2648
2684
  localPath: z.string().optional().describe('Requerido para los 4 kinds -- ruta absoluta del checkout en esa máquina'),
@@ -2657,7 +2693,14 @@ export function createServer() {
2657
2693
  plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- recordatorio manual de "próxima corrida planeada". Ningún kind tiene scheduling real (todo trigger es manual/push): esto NO dispara ninguna corrida sola, solo alimenta el calendario global.'),
2658
2694
  perfUrl: z.string().optional().describe('Requerido si kind="performance" -- URL viva (ej. staging/prod) contra la que corre Lighthouse'),
2659
2695
  maxBundleSizeKb: z.number().optional().describe('Específico de kind="performance" -- umbral en KB para el paso de tamaño de build (default 5000)'),
2660
- minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse (0-100) para pasar (default 50)'),
2696
+ minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse en Performance (0-100) para pasar (default 50)'),
2697
+ perfUrls: z.array(z.string()).optional().describe('Específico de kind="performance" -- URLs ADICIONALES a perfUrl (ej. /precios, /blog): Lighthouse corre un paso por cada una'),
2698
+ minAccessibilityScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Accesibilidad (0-100, default 90)'),
2699
+ minBestPracticesScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Buenas prácticas (0-100, default 90)'),
2700
+ minSeoScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de SEO (0-100, default 90)'),
2701
+ maxLcpMs: z.number().optional().describe('Específico de kind="performance" -- LCP máximo en ms (default 2500, umbral "bueno" de Google)'),
2702
+ maxCls: z.number().optional().describe('Específico de kind="performance" -- CLS máximo (default 0.1)'),
2703
+ maxTbtMs: z.number().optional().describe('Específico de kind="performance" -- TBT máximo en ms (default 200, proxy de laboratorio de INP)'),
2661
2704
  })
2662
2705
 
2663
2706
  registerUpdate('update_agent', '/v1/agents', 'Edita un agente existente.', {
@@ -2678,6 +2721,13 @@ export function createServer() {
2678
2721
  perfUrl: z.string().optional().describe('Ver create_agent -- específico de kind="performance"'),
2679
2722
  maxBundleSizeKb: z.number().optional(),
2680
2723
  minPerfScore: z.number().optional(),
2724
+ perfUrls: z.array(z.string()).optional().describe('Ver create_agent -- reemplaza la lista completa, [] la vacía'),
2725
+ minAccessibilityScore: z.number().optional(),
2726
+ minBestPracticesScore: z.number().optional(),
2727
+ minSeoScore: z.number().optional(),
2728
+ maxLcpMs: z.number().optional(),
2729
+ maxCls: z.number().optional(),
2730
+ maxTbtMs: z.number().optional(),
2681
2731
  })
2682
2732
 
2683
2733
  registerCreate('create_agent_run', '/v1/agent-runs', 'Dispara una corrida de agente (qa: lint/tests/build/E2E; security: npm audit + gitleaks; performance: build size + Lighthouse; screenshots: captura de pantallas para el checklist de publicación) en la rama indicada. Necesita al menos un agente activo para el stack (o falla explícito -- ver list_agents/create_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_agent_run.', {
package/src/setup.mjs CHANGED
@@ -78,22 +78,72 @@ export async function runSetup() {
78
78
  const agentLabelAnswer = await prompt('¿Cómo se llama este agente/herramienta? [Claude Code]: ')
79
79
  const agentLabel = agentLabelAnswer.trim() || 'Claude Code'
80
80
 
81
- const res = await fetch(`${apiBase}/v1/bootstrap`, {
82
- method: 'POST',
83
- headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
84
- body: JSON.stringify({ agentLabel }),
85
- })
86
- const body = await res.json()
87
- if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
81
+ // askIfMissing: si no hay workspace la API no crea nada solo -- avisa
82
+ // (needsWorkspace) y aquí se le pide el nombre al usuario.
83
+ const callBootstrap = async (extra = {}) => {
84
+ const res = await fetch(`${apiBase}/v1/bootstrap`, {
85
+ method: 'POST',
86
+ headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ agentLabel, askIfMissing: true, ...extra }),
88
+ })
89
+ const body = await res.json()
90
+ if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
91
+ return body
92
+ }
93
+ const askName = async (question) => {
94
+ for (;;) {
95
+ const name = (await prompt(question)).trim()
96
+ if (name) return name
97
+ console.log('El nombre no puede estar vacío.')
98
+ }
99
+ }
100
+
101
+ let body = await callBootstrap()
102
+ if (body.needsWorkspaceChoice) {
103
+ // Miembro de varios workspaces: elegir aquí no cambia el workspace
104
+ // actual de la web, solo a cuál queda conectada esta carpeta.
105
+ const { workspaces, currentWorkspaceId } = body
106
+ console.log('\nPerteneces a varios workspaces. ¿En cuál quieres trabajar desde esta carpeta?')
107
+ workspaces.forEach((w, i) => console.log(` ${i + 1}. ${w.name}${w.id === currentWorkspaceId ? ' (el que tienes abierto en la web)' : ''}`))
108
+ const currentIdx = workspaces.findIndex(w => w.id === currentWorkspaceId) + 1
109
+ let idx
110
+ for (;;) {
111
+ const answer = (await prompt(currentIdx ? `Número [Enter para usar "${workspaces[currentIdx - 1].name}"]: ` : 'Número: ')).trim()
112
+ idx = answer === '' && currentIdx ? currentIdx : parseInt(answer, 10)
113
+ if (idx >= 1 && idx <= workspaces.length) break
114
+ console.log('Selección inválida.')
115
+ }
116
+ body = await callBootstrap({ workspaceId: workspaces[idx - 1].id })
117
+ } else if (body.needsWorkspace) {
118
+ console.log('\nTodavía no tienes un workspace en Orquestra. Primero vamos a crearlo')
119
+ console.log('(agrupa todos tus proyectos: tu empresa, tu equipo o tu nombre si trabajas solo).')
120
+ const workspaceName = await askName('Nombre del workspace: ')
121
+ body = await callBootstrap({ workspaceName })
122
+ console.log(`Workspace "${workspaceName}" creado.`)
123
+ }
88
124
 
89
- const { workspaceId, token, created, projects } = body
125
+ const { workspaceId, token, projects } = body
126
+ // La API de producción anterior a ORQ-388 no manda workspaceName.
127
+ const wsLabel = body.workspaceName ? ` del workspace "${body.workspaceName}"` : ''
90
128
  let defaultProjectId
91
129
 
92
- if (created) {
93
- defaultProjectId = body.projectId
94
- console.log(`Se creó un workspace y un proyecto default para ti ("${projects[0].name}") -- puedes renombrarlos cuando quieras desde orquestra-web.`)
130
+ const createProject = async (name) => {
131
+ const res2 = await fetch(`${apiBase}/v1/projects?workspaceId=${workspaceId}`, {
132
+ method: 'POST',
133
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
134
+ body: JSON.stringify({ name }),
135
+ })
136
+ const newProject = await res2.json()
137
+ if (!res2.ok) throw new Error(newProject?.error || `HTTP ${res2.status}`)
138
+ console.log(`Proyecto "${newProject.name}"${wsLabel} creado y configurado como default.`)
139
+ return newProject.id
140
+ }
141
+
142
+ if (projects.length === 0) {
143
+ console.log(`\nAhora crea el proyecto${wsLabel} que este MCP va a usar (puedes agregar más después).`)
144
+ defaultProjectId = await createProject(await askName('Nombre del proyecto: '))
95
145
  } else {
96
- console.log('\nElige el proyecto que este MCP va a usar por default:')
146
+ console.log(`\nElige el proyecto${wsLabel} que este MCP va a usar por default:`)
97
147
  console.log(' 0. (crear un proyecto nuevo)')
98
148
  projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`))
99
149
  const singleDefaultIdx = projects.length === 1 ? 1 : null
@@ -105,20 +155,10 @@ export async function runSetup() {
105
155
  if (Number.isNaN(idx) || idx < 0 || idx > projects.length) throw new Error('Selección inválida')
106
156
 
107
157
  if (idx === 0) {
108
- const name = await prompt('Nombre del proyecto nuevo: ')
109
- if (!name.trim()) throw new Error('El nombre no puede estar vacío')
110
- const res2 = await fetch(`${apiBase}/v1/projects?workspaceId=${workspaceId}`, {
111
- method: 'POST',
112
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
113
- body: JSON.stringify({ name: name.trim() }),
114
- })
115
- const newProject = await res2.json()
116
- if (!res2.ok) throw new Error(newProject?.error || `HTTP ${res2.status}`)
117
- defaultProjectId = newProject.id
118
- console.log(`Proyecto "${newProject.name}" creado y configurado como default.`)
158
+ defaultProjectId = await createProject(await askName('Nombre del proyecto nuevo: '))
119
159
  } else {
120
160
  defaultProjectId = projects[idx - 1].id
121
- console.log(`Usando tu proyecto "${projects[idx - 1].name}" como default.`)
161
+ console.log(`Usando tu proyecto "${projects[idx - 1].name}"${wsLabel} como default.`)
122
162
  }
123
163
  }
124
164
 
@@ -135,12 +175,18 @@ export async function runSetup() {
135
175
 
136
176
  upsertProjectMapping({ apiBase, apiToken: token, workspaceId, folderPath, projectId: defaultProjectId })
137
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'
138
182
  console.log('\nSiguiente paso -- conéctalo a tu agente de IA:\n')
139
183
  console.log(' Claude Code:')
140
- console.log(' claude mcp add orquestra -- npx orquestra-mcp\n')
141
- console.log(' Claude Desktop -- agrega esto a tu claude_desktop_config.json:')
142
- console.log(' { "mcpServers": { "orquestra": { "command": "npx", "args": ["orquestra-mcp"] } } }\n')
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')
143
189
  console.log(' ¿Usas Cursor, ChatGPT/Codex o Gemini CLI? La config de cada uno está en:')
144
- console.log(' https://orquestra.me/mcp#instalacion\n')
145
- console.log('Docs completas y las 35 tools disponibles: https://orquestra.me/mcp')
190
+ console.log(' https://www.orquestra.me/mcp#instalacion\n')
191
+ console.log('Docs completas y todas las tools disponibles: https://www.orquestra.me/mcp')
146
192
  }