orquestra-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 José Alfonso Guerrero Juárez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # orquestra-mcp
2
+
3
+ Servidor MCP (Model Context Protocol) para Orquestra. Expone **35 tools**
4
+ que llaman a la API de `orquestra-infra`: lectura de contexto/dashboard/
5
+ workspace/infra/paridad/listados, y escritura de tareas/ideas/changelog/
6
+ notas/docs/módulos/flujo/categorías/infra/proyectos — un agente puede
7
+ prácticamente todo lo que un humano hace en la web, directo desde la
8
+ conversación. `get_setup_guide` documenta el orden recomendado para
9
+ configurar/sincronizar un proyecto completo sin crear duplicados.
10
+
11
+ Ver `CLAUDE.md` para contexto de desarrollo. Ver la memoria del proyecto
12
+ `orquestra-mcp-api-plan` para las 8 fases del plan MCP/API completo.
13
+
14
+ ## Setup rápido (recomendado)
15
+
16
+ ```bash
17
+ npm install
18
+ node bin/orquestra-mcp.mjs setup
19
+ ```
20
+
21
+ Esto abre tu navegador en `https://app.orquestra.me/cli-auth` para
22
+ confirmar tu sesión (Google o email/password, lo que ya uses) y:
23
+
24
+ - Si **no tienes** workspace/proyecto todavía: te crea uno default
25
+ ("Mi primer proyecto", con los 6 estatus base) — puedes renombrarlo o
26
+ cambiarlo cuando quieras desde `orquestra-web`.
27
+ - Si **ya tienes** uno o varios: te deja elegir cuál usar por default
28
+ desde la misma terminal.
29
+
30
+ Guarda todo (`apiToken`, `workspaceId`, `defaultProjectId`) en
31
+ `~/.orquestra/mcp-config.json` — después de esto, `node bin/orquestra-mcp.mjs`
32
+ (sin más flags ni env vars) ya funciona.
33
+
34
+ Variable opcional `ORQUESTRA_WEB_URL` para apuntar el login a otro lugar
35
+ (ej. `http://localhost:5173` mientras desarrollas `orquestra-web` local en
36
+ vez de contra producción).
37
+
38
+ ## Setup manual (alternativa, sin el flujo de navegador)
39
+
40
+ Si preferís generar el token vos mismo en vez de usar `setup`:
41
+
42
+ 1. **Cuenta + workspace en `orquestra-web`**: entra a la app, si no tienes
43
+ `workspaceId` te manda al wizard de onboarding.
44
+ 2. **Token de servicio**, con esa misma sesión ya logueada:
45
+
46
+ ```bash
47
+ curl -X POST "$API_BASE/v1/tokens" \
48
+ -H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
49
+ -H "Content-Type: application/json" \
50
+ -d '{"workspaceId":"<workspace-id>"}'
51
+ ```
52
+
53
+ El token se muestra **una sola vez** — guárdalo.
54
+ 3. Pasa `ORQUESTRA_API_TOKEN`/`ORQUESTRA_WORKSPACE_ID` como env vars (ver
55
+ abajo) — en este modo `projectId` es obligatorio en cada tool call, ya
56
+ que no hay `defaultProjectId` guardado.
57
+
58
+ ## Configuración
59
+
60
+ Variables de entorno (todas opcionales si ya corriste `setup` — se leen de
61
+ `~/.orquestra/mcp-config.json` como fallback):
62
+
63
+ | Variable | Requerida sin `setup` | Default |
64
+ |---|---|---|
65
+ | `ORQUESTRA_API_TOKEN` | sí | — |
66
+ | `ORQUESTRA_WORKSPACE_ID` | sí | — |
67
+ | `ORQUESTRA_DEFAULT_PROJECT_ID` | no | — |
68
+ | `ORQUESTRA_API_BASE` | no | `ApiBaseUrl` de producción |
69
+ | `ORQUESTRA_WEB_URL` (solo `setup`) | no | `https://app.orquestra.me` |
70
+
71
+ ### Claude Code
72
+
73
+ Si ya corriste `setup`, alcanza con:
74
+
75
+ ```bash
76
+ claude mcp add orquestra -- node /ruta/absoluta/a/orquestra-mcp/bin/orquestra-mcp.mjs
77
+ ```
78
+
79
+ O manual, sin `setup`:
80
+
81
+ ```bash
82
+ claude mcp add orquestra \
83
+ -e ORQUESTRA_API_TOKEN=orq_... \
84
+ -e ORQUESTRA_WORKSPACE_ID=... \
85
+ -- node /ruta/absoluta/a/orquestra-mcp/bin/orquestra-mcp.mjs
86
+ ```
87
+
88
+ ### Claude Desktop (`claude_desktop_config.json`)
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "orquestra": {
94
+ "command": "node",
95
+ "args": ["/ruta/absoluta/a/orquestra-mcp/bin/orquestra-mcp.mjs"]
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ (sin `env` si ya corriste `setup`; si no, agrega `ORQUESTRA_API_TOKEN`/`ORQUESTRA_WORKSPACE_ID` igual que en Claude Code.)
102
+
103
+ ## Tools
104
+
105
+ Lectura de contexto:
106
+
107
+ | Tool | Args | Descripción |
108
+ |---|---|---|
109
+ | `get_project_context` | `projectId?` | Snapshot compacto del proyecto (fase, tareas abiertas/vencidas/bloqueadas, módulos, ideas, changelog reciente) |
110
+ | `list_tasks` | `projectId?`, `statusId?` | Lista de tareas, filtro opcional por estatus |
111
+ | `search` | `projectId?`, `q` | Búsqueda por texto en tareas/notas/ideas/docs |
112
+ | `get_dashboard_summary` | `projectId?` | Hoy/vencidas/bloqueadas, salud de módulos, actividad reciente |
113
+ | `get_infra_status` | `projectId?` | Servicios, entornos y distribuciones con conteos operational/degraded/down |
114
+ | `get_parity_summary` | `projectId?` | Desfases de paridad multiplataforma (tasks y modules por separado) |
115
+ | `get_workspace_summary` | — | Progreso agregado de todos los proyectos del workspace |
116
+ | `get_stage_playbook` | `stageId` | Plantilla de módulo+tareas para una etapa (`pre`\|`build`\|`post`) — sin red |
117
+ | `get_setup_guide` | — | Orden recomendado (flujo→categorías→infra→módulos→tareas→ideas→notas→changelog→docs) para configurar/sincronizar sin duplicar — sin red |
118
+ | `list_modules` / `list_statuses` / `list_categories` / `list_services` / `list_environments` / `list_distributions` | `projectId?` | Lista completa de cada entidad — revísalos antes de `create_*` para no duplicar (patrón upsert) |
119
+
120
+ Creación (`create_*`) y edición (`update_*`) — mismo patrón en todos, solo
121
+ crear/editar, sin `delete_*`:
122
+
123
+ | Entidad | Tools | Notas |
124
+ |---|---|---|
125
+ | Tareas | `create_task`, `update_task` | `update_task` incluye `scope` — úsalo para resolver desfases de paridad |
126
+ | Ideas | `create_idea` | |
127
+ | Changelog | `create_changelog_entry` | Estilo Keep a Changelog |
128
+ | Notas | `create_note` | |
129
+ | Docs | `create_doc` | |
130
+ | Módulos | `create_module`, `update_module` | `scope` también editable |
131
+ | Flujo (estatus) | `create_status`, `update_status` | `nextIds` define las transiciones válidas |
132
+ | Categorías | `create_category`, `update_category` | |
133
+ | Servicios | `create_service`, `update_service` | `update_service` para cambiar `status` tras un incidente |
134
+ | Entornos | `create_environment`, `update_environment` | `update_environment` para reflejar un deploy |
135
+ | Distribuciones | `create_distribution`, `update_distribution` | |
136
+
137
+ Compuestas:
138
+
139
+ | Tool | Args | Descripción |
140
+ |---|---|---|
141
+ | `init_project` | `name`, `description?`, `phase?`, `projectType?`, `distributions?`, `categories?` | Crea un proyecto nuevo en el workspace — categorías+distribuciones+flujo conectado en un paso |
142
+ | `apply_stage_playbook` | `stageId`, `moduleName?`, `includeDates?` | Crea el módulo + todas las tareas de la plantilla de una etapa |
143
+
144
+ Los campos enum (`type`, `priority`, `status`, etc.) están fijos y
145
+ documentados en el `inputSchema` de cada tool — el modelo los ve sin
146
+ adivinar. Todo lo que se crea/edita aparece en vivo en `orquestra-web`
147
+ (misma colección de Firestore, mismo `onSnapshot`).
148
+
149
+ `projectId` es opcional en todas las tools de proyecto si hay un
150
+ `defaultProjectId` configurado (por `setup` o por
151
+ `ORQUESTRA_DEFAULT_PROJECT_ID`) — si no hay ninguno de los dos, la tool
152
+ devuelve error pidiendo uno explícito. `get_workspace_summary` e
153
+ `init_project` no usan `projectId` en absoluto (operan a nivel workspace).
154
+
155
+ El `workspaceId` nunca es un argumento de las tools — viene fijo de la
156
+ configuración, así el token de servicio no puede usarse fuera del
157
+ workspace para el que se creó.
158
+
159
+ ## Probar localmente
160
+
161
+ ```bash
162
+ npm install
163
+ ORQUESTRA_API_TOKEN=orq_... ORQUESTRA_WORKSPACE_ID=... \
164
+ npx @modelcontextprotocol/inspector node bin/orquestra-mcp.mjs
165
+ ```
166
+
167
+ Abre la UI del inspector y llama las tools contra un proyecto real.
168
+
169
+ ## Flujo recomendado para configurar/sincronizar un proyecto
170
+
171
+ Para que un agente pueda "barrer" un proyecto completo (llenar o
172
+ actualizar todo sin crear duplicados), el patrón es:
173
+
174
+ 1. Llamar `get_setup_guide` una vez al inicio de la conversación de setup.
175
+ 2. Para cada sección, en el orden que indica la guía: listar lo existente
176
+ (`list_*`/`search`), decidir por coincidencia de `name`/`title`/`type`
177
+ si ya hay algo — `update_*` si sí, `create_*` si no.
178
+ 3. Respetar el orden: flujo → categorías → infra → módulos → tareas →
179
+ ideas → notas → changelog → docs — los módulos y tareas referencian
180
+ ids (`statusId`, `categoryId`) que deben existir antes.
181
+
182
+ No hay ninguna tool que haga esto automáticamente de punta a punta — la
183
+ guía orienta al agente, pero decidir qué contenido va en cada
184
+ create/update sigue siendo criterio del agente y del humano.
185
+
186
+ ## Estado
187
+
188
+ 35 tools: 8 de lectura (contexto/tasks/search + dashboard/workspace/infra/
189
+ paridad/playbook) + 6 `list_*` + `get_setup_guide` + 18 de creación/
190
+ edición (tasks/ideas/changelog/notes/docs + módulos/flujo/categorías/
191
+ servicios/entornos/distribuciones) + 2 compuestas (init_project,
192
+ apply_stage_playbook). No hay `delete_*`, ni `update_idea`/`update_note`/
193
+ `update_doc`/`update_changelog_entry` todavía. Distribuido por ahora vía
194
+ path local o `npx github:ajuarez93/orquestra-mcp` — publish a npm como
195
+ `orquestra-mcp` queda pendiente (requiere crear el scope `orquestra` en
196
+ npmjs.com).
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { createServer } from '../src/server.mjs'
4
+ import { runSetup } from '../src/setup.mjs'
5
+
6
+ async function main() {
7
+ if (process.argv[2] === 'setup') {
8
+ await runSetup()
9
+ return
10
+ }
11
+
12
+ let server
13
+ try {
14
+ server = createServer()
15
+ } catch (err) {
16
+ console.error(`[orquestra-mcp] ${err.message}`)
17
+ process.exit(1)
18
+ }
19
+
20
+ const transport = new StdioServerTransport()
21
+ await server.connect(transport)
22
+ }
23
+
24
+ main().catch((err) => {
25
+ console.error('[orquestra-mcp] Error fatal:', err)
26
+ process.exit(1)
27
+ })
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "orquestra-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Servidor MCP de Orquestra -- 35 tools para leer y escribir tareas, ideas, changelog, infraestructura y más desde un agente de IA",
5
+ "type": "module",
6
+ "bin": {
7
+ "orquestra-mcp": "./bin/orquestra-mcp.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "homepage": "https://orquestra.me/mcp",
19
+ "keywords": ["mcp", "model-context-protocol", "orquestra", "claude", "ai-agent"],
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.12.0",
23
+ "zod": "^3.23.8"
24
+ }
25
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,82 @@
1
+ import { readConfigFile, CONFIG_PATH } from './configFile.mjs'
2
+
3
+ const DEFAULT_API_BASE = 'https://if9c2vlf35.execute-api.us-east-1.amazonaws.com'
4
+
5
+ export function loadConfig() {
6
+ const fileConfig = readConfigFile()
7
+ const apiBase = process.env.ORQUESTRA_API_BASE || fileConfig?.apiBase || DEFAULT_API_BASE
8
+ const apiToken = process.env.ORQUESTRA_API_TOKEN || fileConfig?.apiToken
9
+ const workspaceId = process.env.ORQUESTRA_WORKSPACE_ID || fileConfig?.workspaceId
10
+ const defaultProjectId = process.env.ORQUESTRA_DEFAULT_PROJECT_ID || fileConfig?.defaultProjectId
11
+
12
+ const missing = []
13
+ if (!apiToken) missing.push('ORQUESTRA_API_TOKEN')
14
+ if (!workspaceId) missing.push('ORQUESTRA_WORKSPACE_ID')
15
+ if (missing.length) {
16
+ throw new Error(
17
+ `Faltan variables de entorno requeridas: ${missing.join(', ')} (y no hay config en ${CONFIG_PATH}). ` +
18
+ `Corre "npx orquestra-mcp setup" para conectarlo con tu cuenta -- crea un workspace/proyecto ` +
19
+ `default si no tienes uno, o te deja elegir uno existente. Alternativa manual: 1) cuenta+workspace ` +
20
+ `ya creados en orquestra-web, 2) generar un token con POST ${apiBase}/v1/tokens con tu ID token ` +
21
+ `de Firebase, y 3) pasar ese token + el workspaceId acá.`
22
+ )
23
+ }
24
+
25
+ return { apiBase, apiToken, workspaceId, defaultProjectId }
26
+ }
27
+
28
+ export class ApiError extends Error {
29
+ constructor(status, message) {
30
+ super(message)
31
+ this.status = status
32
+ }
33
+ }
34
+
35
+ function buildUrl(config, path, params) {
36
+ const url = new URL(path, config.apiBase)
37
+ url.searchParams.set('workspaceId', config.workspaceId)
38
+ for (const [key, value] of Object.entries(params)) {
39
+ if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, value)
40
+ }
41
+ return url
42
+ }
43
+
44
+ async function parseResponse(res) {
45
+ const text = await res.text()
46
+ let body
47
+ try { body = text ? JSON.parse(text) : null } catch { body = text }
48
+
49
+ if (!res.ok) {
50
+ const message = (body && body.error) || (typeof body === 'string' ? body : `HTTP ${res.status}`)
51
+ throw new ApiError(res.status, message)
52
+ }
53
+ return body
54
+ }
55
+
56
+ export async function apiGet(config, path, params = {}) {
57
+ const url = buildUrl(config, path, params)
58
+ const res = await fetch(url, {
59
+ headers: { Authorization: `Bearer ${config.apiToken}` },
60
+ })
61
+ return parseResponse(res)
62
+ }
63
+
64
+ export async function apiPost(config, path, { projectId, ...body }) {
65
+ const url = buildUrl(config, path, { projectId })
66
+ const res = await fetch(url, {
67
+ method: 'POST',
68
+ headers: { Authorization: `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' },
69
+ body: JSON.stringify(body),
70
+ })
71
+ return parseResponse(res)
72
+ }
73
+
74
+ export async function apiPatch(config, path, { projectId, id, ...body }) {
75
+ const url = buildUrl(config, `${path}/${id}`, { projectId })
76
+ const res = await fetch(url, {
77
+ method: 'PATCH',
78
+ headers: { Authorization: `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' },
79
+ body: JSON.stringify(body),
80
+ })
81
+ return parseResponse(res)
82
+ }
@@ -0,0 +1,19 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
2
+ import { homedir } from 'os'
3
+ import { join, dirname } from 'path'
4
+
5
+ export const CONFIG_PATH = join(homedir(), '.orquestra', 'mcp-config.json')
6
+
7
+ export function readConfigFile() {
8
+ if (!existsSync(CONFIG_PATH)) return null
9
+ try {
10
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'))
11
+ } catch {
12
+ return null
13
+ }
14
+ }
15
+
16
+ export function writeConfigFile(data) {
17
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true })
18
+ writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2), { mode: 0o600 })
19
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,560 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import { loadConfig, apiGet, apiPost, apiPatch, ApiError } from './api.mjs'
4
+ import { STAGES, PLAYBOOKS } from './stages.mjs'
5
+
6
+ function errorResult(err) {
7
+ const text = err instanceof ApiError ? `Error (${err.status}): ${err.message}` : `Error: ${err.message}`
8
+ return { content: [{ type: 'text', text }], isError: true }
9
+ }
10
+
11
+ function textResult(data) {
12
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
13
+ }
14
+
15
+ 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.'
16
+
17
+ const TASK_TYPES = ['feature', 'bug', 'improvement', 'tech', 'docs', 'idea', 'meeting', 'call', 'followup', 'business']
18
+ const TASK_PRIORITIES = ['high', 'medium', 'low']
19
+ const CHANGELOG_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']
20
+ const DOC_TYPES = ['readme', 'usecase', 'diagram', 'env', 'adr', 'runbook', 'api']
21
+ const SERVICE_TYPES = ['backend', 'database', 'auth', 'storage', 'push', 'email', 'payments', 'analytics', 'realtime', 'search', 'maps', 'flags', 'admin', 'cdn', 'queue']
22
+ const PLATFORM_TYPES = ['android', 'ios', 'web', 'desktop-mac', 'desktop-win', 'backend', 'extension', 'other']
23
+ const INFRA_STATUSES = ['operational', 'degraded', 'down']
24
+ const STAGE_IDS = STAGES.map(s => s.id)
25
+
26
+ const SETUP_GUIDE = {
27
+ intro: 'Para configurar o sincronizar un proyecto completo: revisa primero con la tool de "check" de cada paso; si ya existe algo que coincide por name/type/title, edítalo con la tool de "update"; si no existe, créalo con la de "create". Sigue el orden -- los pasos 4 en adelante referencian ids de los pasos 1-3 (statusId, categoryId, moduleId, distId).',
28
+ steps: [
29
+ { step: 1, section: 'Flujo (estatus)', check: 'list_statuses', createOrUpdate: 'create_status / update_status', matchBy: 'name' },
30
+ { step: 2, section: 'Categorías', check: 'list_categories', createOrUpdate: 'create_category / update_category', matchBy: 'name' },
31
+ { step: 3, section: 'Infraestructura', check: 'list_services / list_environments / list_distributions (o get_infra_status para los 3 juntos)', createOrUpdate: 'create_service|environment|distribution / update_*', matchBy: 'name (o type para servicios/distribuciones)' },
32
+ { step: 4, section: 'Módulos', check: 'list_modules', createOrUpdate: 'create_module / update_module', matchBy: 'name', dependsOn: ['statusId (paso 1)', 'categoryId (paso 2)'] },
33
+ { step: 5, section: 'Tareas', check: 'list_tasks', createOrUpdate: 'create_task / update_task', matchBy: 'title', dependsOn: ['statusId', 'categoryId', 'moduleId'] },
34
+ { step: 6, section: 'Ideas', check: 'search', createOrUpdate: 'create_idea (sin update_idea todavía)', matchBy: 'title' },
35
+ { step: 7, section: 'Notas', check: 'search', createOrUpdate: 'create_note (sin update_note todavía)', matchBy: 'title' },
36
+ { step: 8, section: 'Changelog', check: 'get_dashboard_summary (recent.changelog) o search', createOrUpdate: 'create_changelog_entry (sin update todavía)', matchBy: 'text' },
37
+ { step: 9, section: 'Docs', check: 'search', createOrUpdate: 'create_doc (sin update_doc todavía)', matchBy: 'title' },
38
+ ],
39
+ }
40
+
41
+ export function createServer() {
42
+ const config = loadConfig()
43
+ const server = new McpServer({ name: 'orquestra-mcp', version: '1.0.0' })
44
+
45
+ function resolveProjectId(projectId) {
46
+ if (config.defaultProjectId) return config.defaultProjectId
47
+ if (!projectId) throw new Error('Falta projectId -- no hay proyecto default configurado, corre "npx orquestra-mcp setup" o pasa projectId explícito.')
48
+ return projectId
49
+ }
50
+
51
+ function registerRead(name, path, description, extraSchema, buildParams) {
52
+ server.registerTool(
53
+ name,
54
+ { description, inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC), ...extraSchema } },
55
+ async (args) => {
56
+ try {
57
+ const data = await apiGet(config, path, buildParams(args, resolveProjectId(args.projectId)))
58
+ return textResult(data)
59
+ } catch (err) {
60
+ return errorResult(err)
61
+ }
62
+ }
63
+ )
64
+ }
65
+
66
+ function registerCreate(name, path, description, extraSchema) {
67
+ server.registerTool(
68
+ name,
69
+ { description, inputSchema: { projectId: z.string().optional().describe(PROJECT_ID_DESC), ...extraSchema } },
70
+ async ({ projectId, ...body }) => {
71
+ try {
72
+ const data = await apiPost(config, path, { projectId: resolveProjectId(projectId), ...body })
73
+ return textResult(data)
74
+ } catch (err) {
75
+ return errorResult(err)
76
+ }
77
+ }
78
+ )
79
+ }
80
+
81
+ function registerUpdate(name, path, description, extraSchema) {
82
+ server.registerTool(
83
+ name,
84
+ {
85
+ description,
86
+ inputSchema: {
87
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
88
+ id: z.string().describe('ID del registro a editar'),
89
+ ...extraSchema,
90
+ },
91
+ },
92
+ async ({ projectId, id, ...body }) => {
93
+ try {
94
+ const data = await apiPatch(config, path, { projectId: resolveProjectId(projectId), id, ...body })
95
+ return textResult(data)
96
+ } catch (err) {
97
+ return errorResult(err)
98
+ }
99
+ }
100
+ )
101
+ }
102
+
103
+ // ═══ LECTURA (existentes) ══════════════════════════════════════════════════
104
+
105
+ registerRead(
106
+ 'get_project_context',
107
+ '/v1/context',
108
+ 'Resumen compacto del estado de un proyecto de Orquestra: fase, plataformas, tareas abiertas/vencidas/bloqueadas, módulos, ideas y changelog reciente.',
109
+ {},
110
+ (args, projectId) => ({ projectId })
111
+ )
112
+
113
+ registerRead(
114
+ 'list_tasks',
115
+ '/v1/tasks',
116
+ 'Lista las tareas de un proyecto de Orquestra, opcionalmente filtradas por estatus.',
117
+ { statusId: z.string().optional().describe('ID de estatus para filtrar (opcional)') },
118
+ (args, projectId) => ({ projectId, statusId: args.statusId })
119
+ )
120
+
121
+ registerRead(
122
+ 'search',
123
+ '/v1/search',
124
+ 'Busca por texto en tareas, notas, ideas y docs de un proyecto de Orquestra.',
125
+ { q: z.string().describe('Texto a buscar') },
126
+ (args, projectId) => ({ projectId, q: args.q })
127
+ )
128
+
129
+ // ═══ LECTURA (nivel A -- dashboard, workspace, infra, paridad, playbooks) ═══
130
+
131
+ registerRead(
132
+ 'get_dashboard_summary',
133
+ '/v1/dashboard',
134
+ 'Resumen tipo dashboard de un proyecto: tareas de hoy, vencidas, bloqueadas, salud de módulos y actividad reciente.',
135
+ {},
136
+ (args, projectId) => ({ projectId })
137
+ )
138
+
139
+ registerRead(
140
+ 'get_infra_status',
141
+ '/v1/infra',
142
+ 'Estado de infraestructura de un proyecto: servicios compartidos, entornos y distribuciones, con conteos de operational/degraded/down.',
143
+ {},
144
+ (args, projectId) => ({ projectId })
145
+ )
146
+
147
+ registerRead(
148
+ 'get_parity_summary',
149
+ '/v1/parity',
150
+ 'Desfases de paridad multiplataforma: tareas y módulos cuyo scope avanza en una distribución pero sigue pendiente en otra.',
151
+ {},
152
+ (args, projectId) => ({ projectId })
153
+ )
154
+
155
+ server.registerTool(
156
+ 'get_workspace_summary',
157
+ {
158
+ description: 'Progreso agregado de todos los proyectos del workspace configurado (no requiere projectId).',
159
+ },
160
+ async () => {
161
+ try {
162
+ const data = await apiGet(config, '/v1/workspace-summary', {})
163
+ return textResult(data)
164
+ } catch (err) {
165
+ return errorResult(err)
166
+ }
167
+ }
168
+ )
169
+
170
+ registerRead(
171
+ 'list_modules',
172
+ '/v1/modules',
173
+ 'Lista los módulos de un proyecto. Úsala antes de create_module para revisar si ya existe uno con ese nombre.',
174
+ {},
175
+ (args, projectId) => ({ projectId })
176
+ )
177
+
178
+ registerRead(
179
+ 'list_statuses',
180
+ '/v1/statuses',
181
+ 'Lista los estatus (columnas del flujo de tareas) de un proyecto. Úsala antes de create_status para no duplicar.',
182
+ {},
183
+ (args, projectId) => ({ projectId })
184
+ )
185
+
186
+ registerRead(
187
+ 'list_categories',
188
+ '/v1/categories',
189
+ 'Lista las categorías de un proyecto. Úsala antes de create_category para no duplicar.',
190
+ {},
191
+ (args, projectId) => ({ projectId })
192
+ )
193
+
194
+ registerRead(
195
+ 'list_services',
196
+ '/v1/services',
197
+ 'Lista los servicios de infraestructura de un proyecto. Úsala antes de create_service para no duplicar.',
198
+ {},
199
+ (args, projectId) => ({ projectId })
200
+ )
201
+
202
+ registerRead(
203
+ 'list_environments',
204
+ '/v1/environments',
205
+ 'Lista los entornos de un proyecto. Úsala antes de create_environment para no duplicar.',
206
+ {},
207
+ (args, projectId) => ({ projectId })
208
+ )
209
+
210
+ registerRead(
211
+ 'list_distributions',
212
+ '/v1/distributions',
213
+ 'Lista las distribuciones/plataformas de un proyecto. Úsala antes de create_distribution para no duplicar.',
214
+ {},
215
+ (args, projectId) => ({ projectId })
216
+ )
217
+
218
+ server.registerTool(
219
+ 'get_setup_guide',
220
+ {
221
+ description: 'Guía del orden recomendado para configurar o sincronizar un proyecto completo (flujo → categorías → infra → módulos → tareas → ideas → notas → changelog → docs). No llama a la API -- datos estáticos.',
222
+ },
223
+ async () => textResult(SETUP_GUIDE)
224
+ )
225
+
226
+ server.registerTool(
227
+ 'get_stage_playbook',
228
+ {
229
+ description: 'Plantilla de módulo + tareas para una etapa del proyecto (pre-proyecto, proyecto, post-proyecto). No llama a la API -- datos estáticos.',
230
+ inputSchema: { stageId: z.enum(STAGE_IDS).describe('pre (idea/diseño) | build (mvp/alpha/beta) | post (launch en adelante)') },
231
+ },
232
+ async ({ stageId }) => {
233
+ const stage = STAGES.find(s => s.id === stageId)
234
+ const playbook = PLAYBOOKS[stageId]
235
+ return textResult({ stage, playbook })
236
+ }
237
+ )
238
+
239
+ // ═══ ESCRITURA (existentes) ═════════════════════════════════════════════════
240
+
241
+ registerCreate(
242
+ 'create_task',
243
+ '/v1/tasks',
244
+ 'Crea una tarea en un proyecto de Orquestra.',
245
+ {
246
+ title: z.string().describe('Título de la tarea'),
247
+ description: z.string().optional(),
248
+ type: z.enum(TASK_TYPES).optional().describe('Default: feature'),
249
+ priority: z.enum(TASK_PRIORITIES).optional().describe('Default: medium'),
250
+ statusId: z.string().optional().describe('Default: el primer estatus del proyecto'),
251
+ categoryId: z.string().optional(),
252
+ moduleId: z.string().optional(),
253
+ dueDate: z.string().optional().describe('YYYY-MM-DD'),
254
+ assignedTo: z.string().optional().describe('uid del miembro asignado'),
255
+ tags: z.array(z.string()).optional(),
256
+ }
257
+ )
258
+
259
+ registerUpdate(
260
+ 'update_task',
261
+ '/v1/tasks',
262
+ 'Edita una tarea existente (incluido su scope multiplataforma, para resolver desfases de paridad).',
263
+ {
264
+ title: z.string().optional(),
265
+ description: z.string().optional(),
266
+ type: z.enum(TASK_TYPES).optional(),
267
+ priority: z.enum(TASK_PRIORITIES).optional(),
268
+ statusId: z.string().optional(),
269
+ categoryId: z.string().optional(),
270
+ moduleId: z.string().optional(),
271
+ dueDate: z.string().optional().describe('YYYY-MM-DD'),
272
+ assignedTo: z.string().optional(),
273
+ tags: z.array(z.string()).optional(),
274
+ scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
275
+ .describe('Alcance por distribución -- usa get_parity_summary para ver el scope actual antes de editarlo'),
276
+ }
277
+ )
278
+
279
+ registerCreate(
280
+ 'create_idea',
281
+ '/v1/ideas',
282
+ 'Crea una idea en un proyecto de Orquestra.',
283
+ {
284
+ title: z.string().describe('Título de la idea'),
285
+ description: z.string().optional(),
286
+ priority: z.enum(TASK_PRIORITIES).optional().describe('Default: medium'),
287
+ categoryId: z.string().optional(),
288
+ dueDate: z.string().optional().describe('YYYY-MM-DD'),
289
+ }
290
+ )
291
+
292
+ registerCreate(
293
+ 'create_changelog_entry',
294
+ '/v1/changelog',
295
+ 'Crea una entrada de changelog en un proyecto de Orquestra (estilo Keep a Changelog).',
296
+ {
297
+ section: z.enum(CHANGELOG_SECTIONS).describe('Sección del changelog'),
298
+ text: z.string().describe('Descripción de la entrada'),
299
+ version: z.string().optional(),
300
+ }
301
+ )
302
+
303
+ registerCreate(
304
+ 'create_note',
305
+ '/v1/notes',
306
+ 'Crea una nota en un proyecto de Orquestra.',
307
+ {
308
+ title: z.string().describe('Título de la nota'),
309
+ body: z.string().optional(),
310
+ pinned: z.boolean().optional(),
311
+ }
312
+ )
313
+
314
+ registerCreate(
315
+ 'create_doc',
316
+ '/v1/docs',
317
+ 'Crea un documento en un proyecto de Orquestra.',
318
+ {
319
+ type: z.enum(DOC_TYPES).describe('Tipo de documento'),
320
+ title: z.string().describe('Título del documento'),
321
+ content: z.string().optional(),
322
+ }
323
+ )
324
+
325
+ // ═══ ESCRITURA (nivel B -- módulos, flujo, categorías, infra) ═══════════════
326
+
327
+ const SCOPE_SCHEMA = z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean().optional() })).optional()
328
+
329
+ registerCreate('create_module', '/v1/modules', 'Crea un módulo en un proyecto de Orquestra.', {
330
+ name: z.string().describe('Nombre del módulo'),
331
+ description: z.string().optional(),
332
+ statusId: z.string().optional(),
333
+ categoryId: z.string().optional(),
334
+ scope: SCOPE_SCHEMA.describe('Alcance por distribución, ej: [{distId, layer, done}]'),
335
+ tags: z.array(z.string()).optional(),
336
+ dueDate: z.string().optional().describe('YYYY-MM-DD'),
337
+ })
338
+ registerUpdate('update_module', '/v1/modules', 'Edita un módulo existente (incluido su scope multiplataforma).', {
339
+ name: z.string().optional(),
340
+ description: z.string().optional(),
341
+ statusId: z.string().optional(),
342
+ categoryId: z.string().optional(),
343
+ scope: SCOPE_SCHEMA,
344
+ tags: z.array(z.string()).optional(),
345
+ dueDate: z.string().optional().describe('YYYY-MM-DD'),
346
+ })
347
+
348
+ registerCreate('create_status', '/v1/statuses', 'Crea un estatus (columna del flujo de tareas) en un proyecto.', {
349
+ name: z.string().describe('Nombre del estatus'),
350
+ color: z.string().describe('Color hex, ej: #5E6AD2'),
351
+ order: z.number().optional().describe('Orden en el flujo, default 0'),
352
+ isInitial: z.boolean().optional().describe('Si es el estatus por default de tareas nuevas'),
353
+ isFinal: z.boolean().optional().describe('Si representa "terminado"'),
354
+ isCancel: z.boolean().optional(),
355
+ isArchive: z.boolean().optional(),
356
+ nextIds: z.array(z.string()).optional().describe('IDs de estatus a los que se puede transicionar desde este'),
357
+ })
358
+ registerUpdate('update_status', '/v1/statuses', 'Edita un estatus existente (nombre, color, orden, transiciones).', {
359
+ name: z.string().optional(),
360
+ color: z.string().optional(),
361
+ order: z.number().optional(),
362
+ isInitial: z.boolean().optional(),
363
+ isFinal: z.boolean().optional(),
364
+ isCancel: z.boolean().optional(),
365
+ isArchive: z.boolean().optional(),
366
+ nextIds: z.array(z.string()).optional(),
367
+ })
368
+
369
+ registerCreate('create_category', '/v1/categories', 'Crea una categoría (agrupación/audiencia) en un proyecto.', {
370
+ name: z.string().describe('Nombre de la categoría'),
371
+ color: z.string().optional().describe('Color hex'),
372
+ })
373
+ registerUpdate('update_category', '/v1/categories', 'Edita una categoría existente.', {
374
+ name: z.string().optional(),
375
+ color: z.string().optional(),
376
+ })
377
+
378
+ // Campos específicos por tipo de servicio -- unión deduplicada de las
379
+ // ~15 categorías (ver SERVICE_SPECIFIC_FIELDS en
380
+ // orquestra-web/src/pages/InfraPanel.jsx, mapa hoy duplicado a mano en
381
+ // 3 lados: web, entities.mjs de la API, y acá -- si cambia uno, cambiar
382
+ // los 3). Solo aplican los que correspondan a `type`; el resto se ignora
383
+ // en el doc. 'authType', no 'type' -- ese nombre ya lo usa el propio
384
+ // catálogo del servicio (bug real encontrado y corregido el 8 ago antes
385
+ // de exponerlo aquí).
386
+ const SERVICE_SPECIFIC_SCHEMA = {
387
+ technology: z.string().optional().describe('Stack -- backend'),
388
+ hosting: z.string().optional().describe('Hosting -- backend'),
389
+ apiUrl: z.string().optional().describe('API URL -- backend'),
390
+ region: z.string().optional().describe('Región, ej. us-east-1 -- backend/database'),
391
+ engine: z.string().optional().describe('Motor, ej. PostgreSQL 15 -- database'),
392
+ provider: z.string().optional().describe('Proveedor -- database/storage/push/email/payments/analytics/realtime/search/maps/flags/cdn/queue'),
393
+ backups: z.string().optional().describe('Frecuencia de backups -- database'),
394
+ bucket: z.string().optional().describe('Bucket -- storage'),
395
+ cdnUrl: z.string().optional().describe('CDN URL -- storage'),
396
+ stores: z.string().optional().describe('Qué guarda -- storage'),
397
+ platforms: z.string().optional().describe('Plataformas cubiertas -- push'),
398
+ authType: z.string().optional().describe('Tipo de auth, ej. JWT/Firebase Auth -- auth'),
399
+ methods: z.string().optional().describe('Social login -- auth'),
400
+ domain: z.string().optional().describe('Dominio -- email/cdn'),
401
+ transactional: z.string().optional().describe('Transaccional -- email'),
402
+ currencies: z.string().optional().describe('Monedas -- payments'),
403
+ mode: z.string().optional().describe('Modo live/test -- payments'),
404
+ events: z.string().optional().describe('Eventos rastreados -- analytics'),
405
+ protocol: z.string().optional().describe('Protocolo -- realtime'),
406
+ index: z.string().optional().describe('Índice -- search'),
407
+ usage: z.string().optional().describe('Uso, ej. geocoding/rutas -- maps'),
408
+ flags: z.string().optional().describe('Flags activos -- flags'),
409
+ stack: z.string().optional().describe('Stack del panel -- admin'),
410
+ url: z.string().optional().describe('URL -- admin'),
411
+ queues: z.string().optional().describe('Colas activas -- queue'),
412
+ details: z.string().optional().describe('Detalles libres -- tipos sin campos propios'),
413
+ }
414
+
415
+ registerCreate('create_service', '/v1/services', 'Crea un servicio compartido de infraestructura (backend, base de datos, auth, etc.) en un proyecto. Además de status/notes, acepta los campos específicos de cada tipo (ej. apiUrl/region para backend, engine/provider para database) -- manda solo los que apliquen a `type`.', {
416
+ type: z.enum(SERVICE_TYPES).describe('Tipo de servicio'),
417
+ name: z.string().describe('Nombre del servicio'),
418
+ status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
419
+ notes: z.string().optional(),
420
+ ...SERVICE_SPECIFIC_SCHEMA,
421
+ })
422
+ registerUpdate('update_service', '/v1/services', 'Edita un servicio de infraestructura existente (status, campos específicos por tipo, incidente...).', {
423
+ type: z.enum(SERVICE_TYPES).optional(),
424
+ name: z.string().optional(),
425
+ status: z.enum(INFRA_STATUSES).optional(),
426
+ notes: z.string().optional(),
427
+ lastIncident: z.string().optional(),
428
+ ...SERVICE_SPECIFIC_SCHEMA,
429
+ })
430
+
431
+ registerCreate('create_environment', '/v1/environments', 'Crea un entorno (producción, staging, desarrollo) en un proyecto.', {
432
+ name: z.string().describe('Nombre del entorno'),
433
+ url: z.string().optional(),
434
+ version: z.string().optional(),
435
+ branch: z.string().optional(),
436
+ status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
437
+ notes: z.string().optional(),
438
+ lastDeploy: z.string().optional().describe('Texto libre, ej. "hace 2 días" -- no es una fecha estructurada'),
439
+ })
440
+ registerUpdate('update_environment', '/v1/environments', 'Edita un entorno existente (útil para actualizar su status/version tras un deploy).', {
441
+ name: z.string().optional(),
442
+ url: z.string().optional(),
443
+ version: z.string().optional(),
444
+ branch: z.string().optional(),
445
+ status: z.enum(INFRA_STATUSES).optional(),
446
+ notes: z.string().optional(),
447
+ lastDeploy: z.string().optional(),
448
+ })
449
+
450
+ registerCreate('create_distribution', '/v1/distributions', 'Crea una distribución/plataforma (iOS, Android, Web, etc.) en un proyecto.', {
451
+ type: z.enum(PLATFORM_TYPES).describe('Tipo de plataforma'),
452
+ label: z.string().describe('Nombre a mostrar por default'),
453
+ name: z.string().optional().describe('Nombre editable -- si no se manda, la UI usa label'),
454
+ categoryId: z.string().optional(),
455
+ status: z.enum(INFRA_STATUSES).optional().describe('Default: operational'),
456
+ phase: z.string().optional().describe('idea | design | alpha | beta | live | deprecated'),
457
+ identifier: z.string().optional().describe('Legado -- preferir bundleId (no-web) o domain (web)'),
458
+ bundleId: z.string().optional().describe('Bundle ID / package name -- plataformas no-web'),
459
+ domain: z.string().optional().describe('Dominio -- plataforma web'),
460
+ hosting: z.string().optional().describe('Hosting -- web/backend'),
461
+ deployMode: z.enum(['auto', 'manual']).optional().describe('Modo de deploy -- web'),
462
+ hasPush: z.boolean().optional().describe('Tiene push notifications -- apps (ios/android)'),
463
+ storeUrl: z.string().optional(),
464
+ stack: z.array(z.string()).optional(),
465
+ version: z.string().optional().describe('Versión en store'),
466
+ reviewVersion: z.string().optional().describe('Build en revisión -- apps'),
467
+ })
468
+ registerUpdate('update_distribution', '/v1/distributions', 'Edita una distribución existente.', {
469
+ label: z.string().optional(),
470
+ name: z.string().optional(),
471
+ categoryId: z.string().optional(),
472
+ status: z.enum(INFRA_STATUSES).optional(),
473
+ phase: z.string().optional(),
474
+ identifier: z.string().optional(),
475
+ bundleId: z.string().optional(),
476
+ domain: z.string().optional(),
477
+ hosting: z.string().optional(),
478
+ deployMode: z.enum(['auto', 'manual']).optional(),
479
+ hasPush: z.boolean().optional(),
480
+ storeUrl: z.string().optional(),
481
+ stack: z.array(z.string()).optional(),
482
+ version: z.string().optional(),
483
+ reviewVersion: z.string().optional(),
484
+ })
485
+
486
+ // ═══ COMPUESTAS (nivel C) ═══════════════════════════════════════════════════
487
+
488
+ server.registerTool(
489
+ 'init_project',
490
+ {
491
+ description: 'Crea un proyecto nuevo dentro del workspace configurado (categorías, distribuciones y los 6 estatus del flujo ya conectados).',
492
+ inputSchema: {
493
+ name: z.string().describe('Nombre del proyecto'),
494
+ description: z.string().optional(),
495
+ phase: z.string().optional().describe('idea | mvp | v1 | v2 | beta | produccion (default: idea)'),
496
+ projectType: z.enum(['code', 'content', 'hybrid']).optional().describe('Default: hybrid'),
497
+ url: z.string().optional(),
498
+ distributions: z.array(z.enum(PLATFORM_TYPES)).optional().describe('Plataformas a crear, ej: ["web","ios"]'),
499
+ categories: z.array(z.string()).optional().describe('Nombres de categorías a crear (default: ["General"])'),
500
+ },
501
+ },
502
+ async (body) => {
503
+ try {
504
+ const data = await apiPost(config, '/v1/projects', body)
505
+ return textResult(data)
506
+ } catch (err) {
507
+ return errorResult(err)
508
+ }
509
+ }
510
+ )
511
+
512
+ server.registerTool(
513
+ 'apply_stage_playbook',
514
+ {
515
+ description: 'Crea el módulo y las tareas de la plantilla de una etapa (pre-proyecto, proyecto, post-proyecto) en un proyecto real.',
516
+ inputSchema: {
517
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
518
+ stageId: z.enum(STAGE_IDS).describe('pre | build | post -- ver get_stage_playbook para el contenido'),
519
+ moduleName: z.string().optional().describe('Nombre del módulo -- default: el de la plantilla'),
520
+ includeDates: z.boolean().optional().describe('Si asigna dueDate a cada tarea usando los offsetDays de la plantilla (default: false)'),
521
+ },
522
+ },
523
+ async ({ projectId, stageId, moduleName, includeDates }) => {
524
+ try {
525
+ const pid = resolveProjectId(projectId)
526
+ const playbook = PLAYBOOKS[stageId]
527
+ const moduleData = await apiPost(config, '/v1/modules', {
528
+ projectId: pid,
529
+ name: moduleName || playbook.moduleName,
530
+ description: playbook.moduleDescription,
531
+ tags: playbook.moduleTags,
532
+ })
533
+
534
+ const taskIds = []
535
+ for (const t of playbook.tasks) {
536
+ const dueDate = includeDates
537
+ ? new Date(Date.now() + t.offsetDays * 86400000).toISOString().slice(0, 10)
538
+ : undefined
539
+ const created = await apiPost(config, '/v1/tasks', {
540
+ projectId: pid,
541
+ title: t.title,
542
+ description: t.description,
543
+ type: t.type,
544
+ priority: t.priority,
545
+ tags: t.tags,
546
+ moduleId: moduleData.id,
547
+ dueDate,
548
+ })
549
+ taskIds.push(created.id)
550
+ }
551
+
552
+ return textResult({ module: moduleData, tasksCreated: taskIds.length, taskIds })
553
+ } catch (err) {
554
+ return errorResult(err)
555
+ }
556
+ }
557
+ )
558
+
559
+ return server
560
+ }
package/src/setup.mjs ADDED
@@ -0,0 +1,104 @@
1
+ import { createServer } from 'http'
2
+ import { randomBytes } from 'crypto'
3
+ import { exec } from 'child_process'
4
+ import { createInterface } from 'readline'
5
+ import { writeConfigFile, CONFIG_PATH } from './configFile.mjs'
6
+
7
+ const DEFAULT_API_BASE = 'https://if9c2vlf35.execute-api.us-east-1.amazonaws.com'
8
+ const DEFAULT_WEB_URL = 'https://app.orquestra.me'
9
+ const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000
10
+
11
+ function openBrowser(url) {
12
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
13
+ exec(`${cmd} "${url}"`, () => {})
14
+ }
15
+
16
+ function prompt(question) {
17
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
18
+ return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer) }))
19
+ }
20
+
21
+ export async function runSetup() {
22
+ const apiBase = process.env.ORQUESTRA_API_BASE || DEFAULT_API_BASE
23
+ const webUrl = process.env.ORQUESTRA_WEB_URL || DEFAULT_WEB_URL
24
+ const state = randomBytes(16).toString('hex')
25
+
26
+ // Levantamos el server primero para conocer el puerto antes de armar la URL.
27
+ const server = createServer()
28
+ await new Promise(resolve => server.listen(0, resolve))
29
+ const port = server.address().port
30
+ server.close()
31
+
32
+ const idTokenPromise = new Promise((resolve, reject) => {
33
+ const httpServer = createServer((req, res) => {
34
+ res.setHeader('Access-Control-Allow-Origin', '*')
35
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
36
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
37
+
38
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return }
39
+ if (req.method !== 'POST' || req.url !== '/callback') { res.writeHead(404); res.end(); return }
40
+
41
+ let body = ''
42
+ req.on('data', chunk => { body += chunk })
43
+ req.on('end', () => {
44
+ try {
45
+ const { state: gotState, idToken } = JSON.parse(body)
46
+ if (gotState !== state || !idToken) throw new Error('state/idToken inválido')
47
+ res.writeHead(200, { 'Content-Type': 'application/json' })
48
+ res.end(JSON.stringify({ ok: true }))
49
+ clearTimeout(timeout)
50
+ httpServer.close()
51
+ resolve(idToken)
52
+ } catch (e) {
53
+ res.writeHead(400, { 'Content-Type': 'application/json' })
54
+ res.end(JSON.stringify({ ok: false, error: e.message }))
55
+ }
56
+ })
57
+ })
58
+
59
+ const timeout = setTimeout(() => {
60
+ httpServer.close()
61
+ reject(new Error('Se agotó el tiempo esperando el login (5 min). Corre "npx orquestra-mcp setup" de nuevo.'))
62
+ }, CALLBACK_TIMEOUT_MS)
63
+
64
+ httpServer.listen(port)
65
+ httpServer.on('error', reject)
66
+ })
67
+
68
+ const authUrl = `${webUrl}/cli-auth?state=${state}&port=${port}`
69
+ console.log('Abriendo el navegador para conectar tu cuenta de Orquestra...')
70
+ console.log(`Si no se abre solo, entra a: ${authUrl}`)
71
+ openBrowser(authUrl)
72
+
73
+ const idToken = await idTokenPromise
74
+ console.log('Login recibido, configurando...')
75
+
76
+ const res = await fetch(`${apiBase}/v1/bootstrap`, {
77
+ method: 'POST',
78
+ headers: { Authorization: `Bearer ${idToken}` },
79
+ })
80
+ const body = await res.json()
81
+ if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
82
+
83
+ const { workspaceId, token, created, projects } = body
84
+ let defaultProjectId
85
+
86
+ if (created) {
87
+ defaultProjectId = body.projectId
88
+ console.log(`Se creó un workspace y un proyecto default para ti ("${projects[0].name}") -- puedes renombrarlos cuando quieras desde orquestra-web.`)
89
+ } else if (projects.length === 1) {
90
+ defaultProjectId = projects[0].id
91
+ console.log(`Usando tu proyecto "${projects[0].name}" como default.`)
92
+ } else {
93
+ console.log('\nElige el proyecto que este MCP va a usar por default:')
94
+ projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`))
95
+ const answer = await prompt('Número: ')
96
+ const idx = parseInt(answer, 10) - 1
97
+ if (idx < 0 || idx >= projects.length) throw new Error('Selección inválida')
98
+ defaultProjectId = projects[idx].id
99
+ }
100
+
101
+ writeConfigFile({ apiBase, apiToken: token, workspaceId, defaultProjectId })
102
+ console.log(`\nListo. Configuración guardada en ${CONFIG_PATH}.`)
103
+ console.log('Ya puedes correr "npx orquestra-mcp" (o configurarlo en tu cliente MCP) sin pasar variables de entorno.')
104
+ }
package/src/stages.mjs ADDED
@@ -0,0 +1,84 @@
1
+ // Puerto exacto de orquestra-web/src/lib/stages.js (STAGES + PLAYBOOKS) --
2
+ // constantes estaticas, sin acceso a red ni Firestore. get_stage_playbook y
3
+ // apply_stage_playbook las leen directo desde aca.
4
+ export const STAGES = [
5
+ {
6
+ id: 'pre',
7
+ label: 'Pre-proyecto',
8
+ color: '#F0A500',
9
+ phases: ['idea', 'diseño'],
10
+ focus: 'Valida el problema y define el alcance antes de escribir código.',
11
+ },
12
+ {
13
+ id: 'build',
14
+ label: 'Proyecto',
15
+ color: '#5E6AD2',
16
+ phases: ['mvp', 'alpha', 'beta'],
17
+ focus: 'Construye por módulos hasta llegar a una beta estable y medible.',
18
+ },
19
+ {
20
+ id: 'post',
21
+ label: 'Post-proyecto',
22
+ color: '#3B8C65',
23
+ phases: ['launch', 'v1', 'v2', 'v3', 'mantenimiento', 'deprecado'],
24
+ focus: 'Lanzamiento, contenido y adquisición — lleva el producto a la gente.',
25
+ },
26
+ ]
27
+
28
+ export function getStageForPhase(phase) {
29
+ return STAGES.find(s => s.phases.includes(phase)) || STAGES[0]
30
+ }
31
+
32
+ export const PLAYBOOKS = {
33
+ pre: {
34
+ moduleName: 'Validación y definición',
35
+ moduleDescription: 'Valida el problema, el público y el alcance antes de construir. Completar este módulo evita construir algo que nadie necesita.',
36
+ moduleTags: ['playbook', 'pre-proyecto'],
37
+ intro: 'Antes de escribir código: valida que el problema existe y define exactamente qué vas a construir.',
38
+ tasks: [
39
+ { title: 'Definir el problema y la propuesta de valor', description: 'En una frase: ¿qué problema resuelves y para quién? Si no cabe en una frase, aún no está claro. Escríbelo en una nota del proyecto.', type: 'business', priority: 'high', tags: ['validación'], offsetDays: 1 },
40
+ { title: 'Identificar público objetivo y casos de uso', description: 'Describe 2-3 perfiles de usuario concretos y el caso de uso principal de cada uno. ¿Quién lo usa, cuándo y por qué?', type: 'business', priority: 'high', tags: ['validación'], offsetDays: 3 },
41
+ { title: 'Analizar 3 competidores o alternativas', description: 'Cómo resuelve la gente este problema hoy (aunque sea con Excel o WhatsApp). Qué hacen bien, qué les falta, y cuál es tu diferencia.', type: 'business', priority: 'medium', tags: ['validación'], offsetDays: 5 },
42
+ { title: 'Validar la idea con 5 usuarios potenciales', description: 'Conversaciones reales, no encuestas. Pregunta por su problema actual, no por tu solución. Registra los hallazgos en Notas.', type: 'followup', priority: 'high', tags: ['validación'], offsetDays: 10 },
43
+ { title: 'Definir alcance del MVP — qué entra y qué NO', description: 'Lista las features del MVP y, más importante, la lista explícita de lo que se queda fuera de la v1. Documéntalo en Docs.', type: 'docs', priority: 'high', tags: ['alcance'], offsetDays: 12 },
44
+ { title: 'Elegir stack y plataformas iniciales', description: 'Decide con qué empiezas (¿solo web?, ¿iOS primero?) y regístralo en Ajustes → Infraestructura como distribuciones con su stack.', type: 'tech', priority: 'medium', tags: ['stack'], offsetDays: 14 },
45
+ { title: 'Definir métricas de éxito del MVP', description: '¿Cómo sabrás que funcionó? Ej: "10 usuarios lo usan 3 veces por semana". Sin esto, el MVP nunca "termina".', type: 'business', priority: 'medium', tags: ['métricas'], offsetDays: 14 },
46
+ ],
47
+ },
48
+ build: {
49
+ moduleName: 'Camino a beta',
50
+ moduleDescription: 'La base técnica y de proceso que necesitas para llegar a una beta estable con usuarios reales dando feedback.',
51
+ moduleTags: ['playbook', 'proyecto'],
52
+ intro: 'Construye con dirección: infraestructura medible, feedback desde el día uno y una beta que puedas poner en manos de usuarios reales.',
53
+ tasks: [
54
+ { title: 'Configurar entornos e infraestructura base', description: 'Backend, base de datos, auth y storage funcionando. Regístralos en la sección Infra con su proveedor y estado.', type: 'tech', priority: 'high', tags: ['infra'], offsetDays: 3 },
55
+ { title: 'Instrumentar analytics y crash reporting', description: 'Desde el primer build: eventos clave (registro, acción principal, retorno) y reporte de crashes. Sin datos no hay decisiones.', type: 'tech', priority: 'high', tags: ['analytics'], offsetDays: 7 },
56
+ { title: 'Implementar autenticación y onboarding', description: 'El flujo de entrada define la primera impresión. Registro simple + un onboarding que lleve al usuario a su primer momento de valor.', type: 'feature', priority: 'high', tags: ['onboarding'], offsetDays: 14 },
57
+ { title: 'Definir flujo de feedback de beta testers', description: '¿Cómo te van a reportar bugs y sugerencias? (formulario, grupo de WhatsApp, botón in-app). Decide dónde aterriza: aquí en Ideas.', type: 'improvement', priority: 'medium', tags: ['beta', 'feedback'], offsetDays: 18 },
58
+ { title: 'Preparar build de beta (TestFlight / track interno / staging)', description: 'Un canal de distribución de beta por plataforma. Registra la versión en review en el panel de Infra.', type: 'tech', priority: 'high', tags: ['beta'], offsetDays: 21 },
59
+ { title: 'Reclutar 10-20 beta testers', description: 'Idealmente del público objetivo que validaste en pre-proyecto. Amigos que no son el público te dirán que "está padre" y nada más.', type: 'business', priority: 'medium', tags: ['beta'], offsetDays: 25 },
60
+ { title: 'Documentar setup del proyecto (README)', description: 'Cómo levantar el proyecto desde cero: dependencias, variables de entorno, comandos. Guárdalo en Docs. Tu yo del futuro lo agradece.', type: 'docs', priority: 'low', tags: ['docs'], offsetDays: 28 },
61
+ { title: 'Checklist de estabilidad pre-lanzamiento', description: 'Crashes < 1%, flujos principales sin bugs conocidos, tiempos de carga aceptables, datos de prueba limpios. Si pasa, estás listo para launch.', type: 'tech', priority: 'high', tags: ['calidad'], offsetDays: 35 },
62
+ ],
63
+ },
64
+ post: {
65
+ moduleName: 'Lanzamiento y crecimiento',
66
+ moduleDescription: 'El plan de marketing paso a paso después de la beta: posicionamiento, canales de lanzamiento, contenido, y el loop de métricas y feedback que convierte un lanzamiento en crecimiento.',
67
+ moduleTags: ['playbook', 'post-proyecto', 'marketing'],
68
+ intro: 'Terminaste la beta — ahora el trabajo es que la gente lo use. Este playbook te lleva del "¿y ahora qué?" a un lanzamiento con canales, contenido y métricas.',
69
+ tasks: [
70
+ { title: 'Redactar el pitch: propuesta de valor en una frase', description: 'La frase que usarás en la tienda, la landing y redes. Fórmula: "[Producto] ayuda a [público] a [resultado] sin [dolor]". Itérala hasta que alguien ajeno la entienda a la primera.', type: 'business', priority: 'high', tags: ['marketing', 'posicionamiento'], offsetDays: 2 },
71
+ { title: 'Preparar assets de tienda (ASO): capturas, descripción, keywords', description: '5-8 capturas que cuenten una historia (no pantallas sueltas), descripción con el pitch arriba, y keywords que tu público realmente busca.', type: 'business', priority: 'high', tags: ['marketing', 'aso'], offsetDays: 5 },
72
+ { title: 'Publicar landing con captura de correos', description: 'Una página: pitch, 3 beneficios, capturas y un CTA (descargar o dejar correo). Los correos capturados son tu canal propio para siempre.', type: 'feature', priority: 'high', tags: ['marketing', 'landing'], offsetDays: 7 },
73
+ { title: 'Configurar analytics de adquisición', description: 'Fuentes de tráfico, conversión de visita→registro→activación. Necesitas saber DE DÓNDE llegan los usuarios que sí se quedan.', type: 'tech', priority: 'medium', tags: ['analytics', 'adquisición'], offsetDays: 8 },
74
+ { title: 'Plan de contenido de lanzamiento (2 semanas)', description: 'Programa las publicaciones en la sección Contenido: teaser, anuncio, demo del caso de uso principal, historia de por qué lo construiste, testimonios de beta testers.', type: 'business', priority: 'high', tags: ['marketing', 'contenido'], offsetDays: 10 },
75
+ { title: 'Lanzar en canales: Product Hunt, comunidades y redes', description: 'Elige 3 canales donde vive tu público (PH, subreddits, grupos de FB/Discord, foros del nicho). Un post honesto de creador funciona mejor que uno de venta.', type: 'business', priority: 'high', tags: ['marketing', 'lanzamiento'], offsetDays: 14 },
76
+ { title: 'Contactar 10 personas con audiencia en el nicho', description: 'Creators, newsletters, podcasts o prensa pequeña del tema. Mensaje corto y personalizado + acceso gratis. Con que respondan 2, ganaste.', type: 'followup', priority: 'medium', tags: ['marketing', 'outreach'], offsetDays: 16 },
77
+ { title: 'Activar loop de feedback de usuarios reales', description: 'Un canal permanente: encuesta in-app, correo de bienvenida que pregunta "¿qué te trajo aquí?", o botón de sugerencias. Todo aterriza en Ideas.', type: 'improvement', priority: 'medium', tags: ['feedback'], offsetDays: 18 },
78
+ { title: 'Definir y medir activación y retención (D1 / D7 / D30)', description: 'Activación: % que llega al momento de valor. Retención: % que regresa al día 1, 7 y 30. Estos números te dicen si tienes producto o solo descargas.', type: 'business', priority: 'medium', tags: ['métricas'], offsetDays: 21 },
79
+ { title: 'Ritmo de contenido post-lanzamiento (3 publicaciones/semana)', description: 'El lanzamiento es un día; el crecimiento es constancia. Deja programadas en Contenido las siguientes 2 semanas: tips de uso, casos, mejoras del changelog.', type: 'business', priority: 'medium', tags: ['marketing', 'contenido'], offsetDays: 24 },
80
+ { title: 'Publicar release notes / changelog público v1', description: 'Convierte tu Changelog en comunicación: qué hay de nuevo, en lenguaje de usuario. Cada release es una excusa para volver a aparecer.', type: 'docs', priority: 'low', tags: ['changelog'], offsetDays: 26 },
81
+ { title: 'Revisión semanal de métricas y próximos pasos', description: 'Reunión recurrente (aunque seas solo tú): adquisición, activación, retención, top feedback. Decide UNA mejora prioritaria por semana.', type: 'meeting', priority: 'medium', tags: ['métricas', 'ritmo'], offsetDays: 28 },
82
+ ],
83
+ },
84
+ }