rootkid0-initializer 0.1.1 → 0.1.3

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.
@@ -24,13 +24,15 @@ Configura todos los servidores MCP en esta carpeta para evitar definiciones dupl
24
24
  El bootstrap del proyecto asume que MCP Notion ya esta preinstalado/configurado por el usuario.
25
25
  El initializer NO instala ni modifica MCP global.
26
26
 
27
+ El bootstrap interactua con Notion solo via MCP (flujo OpenCode/agent), sin llamadas REST directas ni manejo de token en scripts.
28
+
27
29
  Validacion minima del bootstrap:
28
30
 
29
- - `~/.config/opencode/mcp-servers.json`
31
+ - `~/.config/opencode/opencode.json`
30
32
 
31
33
  Condicion minima requerida:
32
34
 
33
- - Debe existir entrada `notion` en el archivo global.
35
+ - Debe existir `mcp.notion` habilitado (tambien soporta `mcp.servers.notion`).
34
36
 
35
37
  Si falta, el init falla con mensaje de correccion porque el setup Notion es automatico en P1.
36
38
 
package/README.md CHANGED
@@ -65,24 +65,27 @@ chmod +x rootkid0-bootstrap/init-project.sh
65
65
 
66
66
  El script crea una carpeta nueva con la estructura actual del repositorio, excluye `rootkid0-bootstrap/` y `automation/`, reemplaza `{{PROJECT_NAME}}`, mantiene `AGENTS.md`, genera un README inicial y ejecuta bootstrap automatico de Notion.
67
67
 
68
- ## Setup automatico de Notion (MVP)
69
-
70
- El init ejecuta bootstrap Notion automaticamente despues de crear el proyecto. Requisitos obligatorios:
68
+ ## Setup automatico de Notion (MVP)
69
+
70
+ El init ejecuta bootstrap Notion automaticamente despues de crear el proyecto en modo MCP-only. Requisitos obligatorios:
71
71
 
72
72
  - MCP Notion preinstalado/configurado por el usuario (el initializer no instala ni modifica MCP global).
73
- - Archivo global `~/.config/opencode/mcp-servers.json` con entrada `notion`.
74
- - Credencial de Notion resuelta desde MCP Notion (o `NOTION_TOKEN` si la defines manualmente).
73
+ - Archivo global `~/.config/opencode/opencode.json` con `mcp.notion` habilitado (tambien soporta `mcp.servers.notion`).
74
+ - OpenCode CLI disponible para ejecutar el flujo de agente que crea la estructura en Notion via MCP.
75
75
  - Variables opcionales:
76
- - `NOTION_PARENT_PAGE_ID` (recomendado para crear bajo una raiz definida)
76
+ - `NOTION_PARENT_PAGE_ID` (si existe, el root se crea debajo de ese parent)
77
77
  - `NOTION_WORKSPACE_NAME` (opcional)
78
+
79
+ No se requiere `NOTION_TOKEN` en los scripts de bootstrap.
78
80
 
79
81
  Resultado del bootstrap:
80
82
 
81
83
  - Crea pagina raiz del proyecto en Notion.
82
84
  - Si existe `NOTION_PARENT_PAGE_ID`, crea la pagina raiz como hija de ese parent.
83
85
  - Si no existe `NOTION_PARENT_PAGE_ID`, crea la pagina raiz a nivel workspace del usuario.
86
+ - Toda la interaccion con Notion se ejecuta unicamente via MCP (sin llamadas REST directas desde bootstrap).
84
87
  - Crea paginas por fase: `01-business` a `07-production` y `99-common`.
85
- - Crea secciones placeholder del modelo multi-DB: `Projects`, `Phases`, `Deliverables`, `Backlog`, `Risks`, `Decisions`, `Incidents`.
86
- - Guarda IDs generados en `99-common/notion-bootstrap.output.json` dentro del proyecto creado.
88
+ - Crea secciones placeholder del modelo multi-DB: `Projects`, `Phases`, `Deliverables`, `Backlog`, `Risks`, `Decisions`, `Incidents`.
89
+ - Guarda IDs generados en `99-common/notion-bootstrap.output.json` dentro del proyecto creado.
87
90
 
88
91
  MCP recomendados para configurar: `context7`, `engram`, `notion`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rootkid0-initializer",
3
- "version": "0.1.1",
4
- "description": "CLI para inicializar proyectos rootkid0.",
3
+ "version": "0.1.3",
4
+ "description": "CLI para inicializar el flujo de proyectos al estilo rootkid0.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
7
  "bin": {
@@ -64,7 +64,7 @@ Proyecto inicializado desde rootkid0-initializer.
64
64
  - Plantillas markdown con placeholders ya resueltos para tu proyecto.
65
65
  - Configuracion inicial en \`99-common/project.config.json\`.
66
66
  - Integracion OpenCode MVP (\`AGENTS.md\`, \`.opencode/\`, AGENTS locales, skills, MCP y agentes por rol).
67
- - Setup automatico de Notion (MVP): usa MCP Notion preconfigurado por el usuario.
67
+ - Setup automatico de Notion (MVP): MCP-only via OpenCode/agent, sin token en scripts.
68
68
  - Salida de IDs Notion en \`99-common/notion-bootstrap.output.json\`.
69
69
 
70
70
  ## Siguientes pasos
@@ -73,7 +73,7 @@ Proyecto inicializado desde rootkid0-initializer.
73
73
  2. Ajusta \`99-common/project.config.json\` segun tu stack y contexto.
74
74
  3. Revisa \`AGENTS.md\` como entrypoint de roles.
75
75
  4. Revisa \`.opencode/README.md\` para el flujo global + subproyectos.
76
- 5. Confirma prerequisito MCP Notion (preinstalado/configurado) en \`.opencode/mcp/README.md\`.
76
+ 5. Confirma prerequisito MCP Notion (mcp.notion habilitado en opencode.json) en \`.opencode/mcp/README.md\`.
77
77
  6. Verifica \`99-common/notion-bootstrap.output.json\`.
78
78
  7. Versiona cambios con Git y define tu backlog inicial.
79
79
  EOF
@@ -58,7 +58,7 @@ Proyecto inicializado desde rootkid0-initializer.
58
58
  - Plantillas markdown con placeholders ya resueltos para tu proyecto.
59
59
  - Configuracion inicial en `99-common/project.config.json`.
60
60
  - Integracion OpenCode MVP (`AGENTS.md`, `.opencode/`, AGENTS locales, skills, MCP y agentes por rol).
61
- - Setup automatico de Notion (MVP): usa MCP Notion preconfigurado por el usuario.
61
+ - Setup automatico de Notion (MVP): MCP-only via OpenCode/agent, sin token en scripts.
62
62
  - Salida de IDs Notion en `99-common/notion-bootstrap.output.json`.
63
63
 
64
64
  ## Siguientes pasos
@@ -67,7 +67,7 @@ Proyecto inicializado desde rootkid0-initializer.
67
67
  2. Ajusta `99-common/project.config.json` segun tu stack y contexto.
68
68
  3. Revisa `AGENTS.md` como entrypoint de roles.
69
69
  4. Revisa `.opencode/README.md` para el flujo global + subproyectos.
70
- 5. Confirma prerequisito MCP Notion (preinstalado/configurado) en `.opencode/mcp/README.md`.
70
+ 5. Confirma prerequisito MCP Notion (mcp.notion habilitado en opencode.json) en `.opencode/mcp/README.md`.
71
71
  6. Verifica `99-common/notion-bootstrap.output.json`.
72
72
  7. Versiona cambios con Git y define tu backlog inicial.
73
73
  "@
@@ -13,126 +13,226 @@ function Fail([string]$Message) {
13
13
  throw $Message
14
14
  }
15
15
 
16
- function Require-Env([string]$Name) {
17
- $value = [Environment]::GetEnvironmentVariable($Name)
18
- if ([string]::IsNullOrWhiteSpace($value)) {
19
- Fail "Variable requerida no definida: $Name. Definila y vuelve a ejecutar init-project."
16
+ function Require-Command([string]$Name) {
17
+ if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
18
+ Fail "Dependencia requerida no encontrada: $Name. Instalala y vuelve a ejecutar init-project."
20
19
  }
21
20
  }
22
21
 
23
- function Resolve-NotionToken([string]$McpFile) {
24
- $token = [Environment]::GetEnvironmentVariable("NOTION_TOKEN")
25
- if (-not [string]::IsNullOrWhiteSpace($token)) {
26
- return $token
22
+ function Resolve-McpConfigFile() {
23
+ $opencodeFile = Join-Path $HOME ".config/opencode/opencode.json"
24
+
25
+ if (Test-Path -LiteralPath $opencodeFile) {
26
+ return $opencodeFile
27
27
  }
28
28
 
29
+ Fail "Prerequisito faltante: no existe $opencodeFile. Configura OpenCode con MCP Notion habilitado y vuelve a ejecutar init-project."
30
+ }
31
+
32
+ function Test-NotionMcpEnabled([string]$McpFile) {
29
33
  try {
30
34
  $raw = Get-Content -Path $McpFile -Raw | ConvertFrom-Json
31
- $fromConfig = $raw.servers.notion.env.NOTION_TOKEN
32
35
  }
33
36
  catch {
34
- $fromConfig = ""
37
+ return $false
35
38
  }
36
39
 
37
- if ($fromConfig -is [string]) {
38
- if ($fromConfig -match '^\$\{(.+)\}$') {
39
- $refVar = $Matches[1]
40
- $resolved = [Environment]::GetEnvironmentVariable($refVar)
41
- if (-not [string]::IsNullOrWhiteSpace($resolved)) {
42
- return $resolved
40
+ if ($null -ne $raw.mcp) {
41
+ if ($null -ne $raw.mcp.notion) {
42
+ if (($raw.mcp.notion -is [bool]) -and ($raw.mcp.notion -eq $false)) {
43
+ return $false
44
+ }
45
+ if (($raw.mcp.notion -isnot [bool]) -and ($null -ne $raw.mcp.notion.enabled) -and ($raw.mcp.notion.enabled -eq $false)) {
46
+ return $false
47
+ }
48
+ return $true
49
+ }
50
+
51
+ if ($null -ne $raw.mcp.servers -and $null -ne $raw.mcp.servers.notion) {
52
+ if (($null -ne $raw.mcp.servers.notion.enabled) -and ($raw.mcp.servers.notion.enabled -eq $false)) {
53
+ return $false
43
54
  }
55
+ return $true
44
56
  }
45
- elseif (-not [string]::IsNullOrWhiteSpace($fromConfig)) {
46
- return $fromConfig
57
+ }
58
+
59
+ if ($null -ne $raw.servers -and $null -ne $raw.servers.notion) {
60
+ if (($null -ne $raw.servers.notion.enabled) -and ($raw.servers.notion.enabled -eq $false)) {
61
+ return $false
47
62
  }
63
+ return $true
48
64
  }
49
65
 
50
- Fail "No se pudo resolver credencial de Notion. Define NOTION_TOKEN o configura servers.notion.env.NOTION_TOKEN en $McpFile."
66
+ return $false
51
67
  }
52
68
 
53
- function Get-NotionPagePayload([string]$ParentMode, [string]$ParentValue, [string]$Title) {
54
- if ($ParentMode -eq "page") {
55
- return @{
56
- parent = @{ page_id = $ParentValue }
57
- properties = @{
58
- title = @{
59
- title = @(
60
- @{
61
- type = "text"
62
- text = @{ content = $Title }
63
- }
64
- )
65
- }
66
- }
67
- } | ConvertTo-Json -Depth 10
68
- }
69
-
70
- if ($ParentMode -eq "workspace") {
71
- return @{
72
- parent = @{ workspace = $true }
73
- properties = @{
74
- title = @{
75
- title = @(
76
- @{
77
- type = "text"
78
- text = @{ content = $Title }
79
- }
80
- )
81
- }
82
- }
83
- } | ConvertTo-Json -Depth 10
69
+ function New-InstructionFile([string]$Path, [string]$RootTitle, [string]$ParentPageId) {
70
+ $content = @"
71
+ Objetivo: crear la estructura base de Notion para un proyecto rootkid0-initializer usando UNICAMENTE herramientas MCP de Notion.
72
+
73
+ Reglas obligatorias:
74
+ - No usar API HTTP directa de Notion.
75
+ - No usar tokens o variables de entorno de Notion.
76
+ - Usar solo las herramientas MCP de Notion disponibles en esta sesion.
77
+ - Responder al final SOLO con un JSON valido, sin markdown y sin texto adicional.
78
+
79
+ Pasos a ejecutar:
80
+ 1) Crear una pagina raiz con titulo exacto: "$RootTitle".
81
+ 2) Si se provee parent_page_id y no esta vacio, crear la pagina raiz como hija de ese page_id.
82
+ 3) Crear bajo la pagina raiz estas paginas de fase:
83
+ - 01-business
84
+ - 02-proposal
85
+ - 03-design
86
+ - 04-management
87
+ - 05-development
88
+ - 06-deployment
89
+ - 07-production
90
+ - 99-common
91
+ 4) Dentro de 99-common crear estas secciones:
92
+ - Projects
93
+ - Phases
94
+ - Deliverables
95
+ - Backlog
96
+ - Risks
97
+ - Decisions
98
+ - Incidents
99
+
100
+ Datos de entrada:
101
+ - root_title: "$RootTitle"
102
+ - parent_page_id: "$ParentPageId"
103
+
104
+ Formato de salida requerido (JSON exacto en estructura):
105
+ {
106
+ "notion_parent_mode": "workspace o page",
107
+ "notion_parent_page_id": "id o vacio",
108
+ "project_root_page_id": "id",
109
+ "phase_pages": {
110
+ "01-business": "id",
111
+ "02-proposal": "id",
112
+ "03-design": "id",
113
+ "04-management": "id",
114
+ "05-development": "id",
115
+ "06-deployment": "id",
116
+ "07-production": "id",
117
+ "99-common": "id"
118
+ },
119
+ "model_sections": {
120
+ "Projects": "id",
121
+ "Phases": "id",
122
+ "Deliverables": "id",
123
+ "Backlog": "id",
124
+ "Risks": "id",
125
+ "Decisions": "id",
126
+ "Incidents": "id"
84
127
  }
128
+ }
129
+ "@
85
130
 
86
- Fail "Modo de parent no soportado para Notion payload: $ParentMode"
131
+ Set-Content -Path $Path -Value $content
87
132
  }
88
133
 
89
- function New-NotionPage([string]$ParentMode, [string]$ParentValue, [string]$Title) {
90
- $headers = @{
91
- Authorization = "Bearer $script:NotionToken"
92
- "Notion-Version" = "2022-06-28"
93
- "Content-Type" = "application/json"
134
+ function Parse-OpenCodeEvents([string]$EventsPath) {
135
+ $textParts = New-Object System.Collections.Generic.List[string]
136
+ $lines = Get-Content -Path $EventsPath
137
+
138
+ foreach ($line in $lines) {
139
+ if ([string]::IsNullOrWhiteSpace($line)) {
140
+ continue
141
+ }
142
+
143
+ try {
144
+ $evt = $line | ConvertFrom-Json
145
+ }
146
+ catch {
147
+ continue
148
+ }
149
+
150
+ if ($evt.type -eq "text" -and $null -ne $evt.part -and -not [string]::IsNullOrWhiteSpace($evt.part.text)) {
151
+ [void]$textParts.Add([string]$evt.part.text)
152
+ }
94
153
  }
95
154
 
96
- $body = Get-NotionPagePayload -ParentMode $ParentMode -ParentValue $ParentValue -Title $Title
155
+ if ($textParts.Count -eq 0) {
156
+ Fail "No se recibio respuesta de texto desde opencode run."
157
+ }
158
+
159
+ $combined = ($textParts -join "`n").Trim()
160
+ if ($combined -match '^```(?:json)?\s*([\s\S]*?)\s*```$') {
161
+ $combined = $Matches[1].Trim()
162
+ }
97
163
 
98
164
  try {
99
- $response = Invoke-RestMethod -Method Post -Uri "https://api.notion.com/v1/pages" -Headers $headers -Body $body
165
+ $result = $combined | ConvertFrom-Json
100
166
  }
101
167
  catch {
102
- $apiMessage = $_.ErrorDetails.Message
103
- if ([string]::IsNullOrWhiteSpace($apiMessage)) {
104
- $apiMessage = $_.Exception.Message
168
+ Fail "OpenCode no devolvio JSON valido para bootstrap Notion."
169
+ }
170
+
171
+ $requiredPhases = @(
172
+ "01-business",
173
+ "02-proposal",
174
+ "03-design",
175
+ "04-management",
176
+ "05-development",
177
+ "06-deployment",
178
+ "07-production",
179
+ "99-common"
180
+ )
181
+
182
+ $requiredSections = @(
183
+ "Projects",
184
+ "Phases",
185
+ "Deliverables",
186
+ "Backlog",
187
+ "Risks",
188
+ "Decisions",
189
+ "Incidents"
190
+ )
191
+
192
+ if ([string]::IsNullOrWhiteSpace($result.project_root_page_id)) {
193
+ Fail "Falta campo obligatorio en respuesta MCP: project_root_page_id"
194
+ }
195
+
196
+ if ($null -eq $result.phase_pages) {
197
+ Fail "Falta campo obligatorio en respuesta MCP: phase_pages"
198
+ }
199
+
200
+ if ($null -eq $result.model_sections) {
201
+ Fail "Falta campo obligatorio en respuesta MCP: model_sections"
202
+ }
203
+
204
+ foreach ($phase in $requiredPhases) {
205
+ $phaseId = $result.phase_pages.$phase
206
+ if ([string]::IsNullOrWhiteSpace($phaseId)) {
207
+ Fail "Falta fase obligatoria en respuesta MCP: $phase"
105
208
  }
106
- Fail "No se pudo crear pagina en Notion ($Title). Detalle: $apiMessage"
107
209
  }
108
210
 
109
- if ([string]::IsNullOrWhiteSpace($response.id)) {
110
- Fail "Notion no devolvio un id valido al crear '$Title'."
211
+ foreach ($section in $requiredSections) {
212
+ $sectionId = $result.model_sections.$section
213
+ if ([string]::IsNullOrWhiteSpace($sectionId)) {
214
+ Fail "Falta seccion obligatoria en respuesta MCP: $section"
215
+ }
111
216
  }
112
217
 
113
- return $response.id
218
+ return $result
114
219
  }
115
220
 
116
221
  if (-not (Test-Path -LiteralPath $ProjectDir)) {
117
222
  Fail "No existe el directorio de proyecto: $ProjectDir"
118
223
  }
119
224
 
120
- $mcpFile = Join-Path $HOME ".config/opencode/mcp-servers.json"
121
-
122
- if (-not (Test-Path -LiteralPath $mcpFile)) {
123
- Fail "Prerequisito faltante: MCP Notion no disponible. Debe existir $mcpFile con entrada notion antes de ejecutar init-project."
124
- }
225
+ Require-Command "opencode"
125
226
 
126
- if (-not (Select-String -Path $mcpFile -Pattern '"notion"' -Quiet)) {
127
- Fail "Prerequisito faltante: MCP Notion no disponible. Agrega entrada notion en $mcpFile y vuelve a ejecutar init-project."
227
+ $mcpFile = Resolve-McpConfigFile
228
+ if (-not (Test-NotionMcpEnabled -McpFile $mcpFile)) {
229
+ Fail "Prerequisito faltante: MCP Notion no habilitado en $mcpFile. Agrega la entrada mcp.notion (o mcp.servers.notion) y vuelve a ejecutar init-project."
128
230
  }
129
231
 
130
- $script:NotionToken = Resolve-NotionToken -McpFile $mcpFile
131
-
132
232
  $workspaceName = [Environment]::GetEnvironmentVariable("NOTION_WORKSPACE_NAME")
133
233
  $parentPageId = [Environment]::GetEnvironmentVariable("NOTION_PARENT_PAGE_ID")
134
234
 
135
- Write-Host "Iniciando bootstrap automatico de Notion..."
235
+ Write-Host "Iniciando bootstrap automatico de Notion via MCP..."
136
236
 
137
237
  $rootTitle = $ProjectName
138
238
  if (-not [string]::IsNullOrWhiteSpace($workspaceName)) {
@@ -140,79 +240,50 @@ if (-not [string]::IsNullOrWhiteSpace($workspaceName)) {
140
240
  }
141
241
 
142
242
  $parentMode = "workspace"
143
- $parentValue = ""
144
-
145
243
  if (-not [string]::IsNullOrWhiteSpace($parentPageId)) {
146
244
  $parentMode = "page"
147
- $parentValue = $parentPageId
148
- Write-Host "Modo parent Notion: page ($parentPageId)"
245
+ Write-Host "Modo parent Notion (opcional): page ($parentPageId)"
149
246
  }
150
247
  else {
151
- Write-Host "Modo parent Notion: workspace (NOTION_PARENT_PAGE_ID no definido)"
248
+ Write-Host "Modo parent Notion (opcional): workspace"
152
249
  }
153
250
 
154
- $projectRootPageId = New-NotionPage -ParentMode $parentMode -ParentValue $parentValue -Title $rootTitle
155
- Write-Host "Pagina raiz creada: $projectRootPageId"
156
-
157
- $phaseNames = @(
158
- "01-business",
159
- "02-proposal",
160
- "03-design",
161
- "04-management",
162
- "05-development",
163
- "06-deployment",
164
- "07-production",
165
- "99-common"
166
- )
251
+ $instructionFile = [System.IO.Path]::GetTempFileName()
252
+ $eventsFile = [System.IO.Path]::GetTempFileName()
167
253
 
168
- $phaseMap = [ordered]@{}
169
- $commonPageId = ""
254
+ try {
255
+ New-InstructionFile -Path $instructionFile -RootTitle $rootTitle -ParentPageId $parentPageId
170
256
 
171
- foreach ($phase in $phaseNames) {
172
- $phaseId = New-NotionPage -ParentMode "page" -ParentValue $projectRootPageId -Title $phase
173
- $phaseMap[$phase] = $phaseId
174
- Write-Host "Pagina creada ($phase): $phaseId"
175
- if ($phase -eq "99-common") {
176
- $commonPageId = $phaseId
257
+ & opencode run --format json --dir $ProjectDir --file $instructionFile "Sigue las instrucciones del archivo adjunto y devuelve SOLO el JSON final." 2>&1 | Set-Content -Path $eventsFile
258
+ if ($LASTEXITCODE -ne 0) {
259
+ Fail "Fallo la ejecucion de OpenCode para bootstrap de Notion. Verifica que MCP Notion este disponible y operativo."
177
260
  }
178
- }
179
261
 
180
- if ([string]::IsNullOrWhiteSpace($commonPageId)) {
181
- Fail "No se pudo crear la pagina 99-common en Notion."
182
- }
262
+ $mcpResult = Parse-OpenCodeEvents -EventsPath $eventsFile
183
263
 
184
- $sectionNames = @(
185
- "Projects",
186
- "Phases",
187
- "Deliverables",
188
- "Backlog",
189
- "Risks",
190
- "Decisions",
191
- "Incidents"
192
- )
264
+ $outputPath = Join-Path $ProjectDir "99-common/notion-bootstrap.output.json"
193
265
 
194
- $sectionMap = [ordered]@{}
266
+ $result = [ordered]@{
267
+ project_name = $ProjectName
268
+ workspace_name = $workspaceName
269
+ created_at_utc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
270
+ notion_parent_mode = if (-not [string]::IsNullOrWhiteSpace($mcpResult.notion_parent_mode)) { $mcpResult.notion_parent_mode } else { $parentMode }
271
+ notion_parent_page_id = if (-not [string]::IsNullOrWhiteSpace($mcpResult.notion_parent_page_id)) { $mcpResult.notion_parent_page_id } else { $parentPageId }
272
+ project_root_page_id = $mcpResult.project_root_page_id
273
+ phase_pages = $mcpResult.phase_pages
274
+ model_sections = $mcpResult.model_sections
275
+ }
195
276
 
196
- foreach ($section in $sectionNames) {
197
- $sectionId = New-NotionPage -ParentMode "page" -ParentValue $commonPageId -Title $section
198
- $sectionMap[$section] = $sectionId
199
- Write-Host "Seccion modelo MVP creada ($section): $sectionId"
200
- }
277
+ $result | ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath
201
278
 
202
- $outputPath = Join-Path $ProjectDir "99-common/notion-bootstrap.output.json"
203
-
204
- $result = [ordered]@{
205
- project_name = $ProjectName
206
- workspace_name = $workspaceName
207
- created_at_utc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
208
- notion_parent_mode = $parentMode
209
- notion_parent_page_id = $parentPageId
210
- project_root_page_id = $projectRootPageId
211
- phase_pages = $phaseMap
212
- model_sections = $sectionMap
279
+ Write-Host "Bootstrap Notion completado via MCP."
280
+ Write-Host "Salida guardada en: $outputPath"
281
+ }
282
+ finally {
283
+ if (Test-Path -LiteralPath $instructionFile) {
284
+ Remove-Item -LiteralPath $instructionFile -Force
285
+ }
286
+ if (Test-Path -LiteralPath $eventsFile) {
287
+ Remove-Item -LiteralPath $eventsFile -Force
288
+ }
213
289
  }
214
-
215
- $result | ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath
216
-
217
- Write-Host "Bootstrap Notion completado."
218
- Write-Host "Salida guardada en: $outputPath"
@@ -34,27 +34,6 @@ pick_python() {
34
34
  return 1
35
35
  }
36
36
 
37
- validate_mcp_config() {
38
- local mcp_file="$HOME/.config/opencode/mcp-servers.json"
39
-
40
- if [[ ! -f "$mcp_file" ]]; then
41
- fail "Prerequisito faltante: MCP Notion no disponible. Debe existir $mcp_file con entrada 'notion' antes de ejecutar init-project."
42
- fi
43
-
44
- if ! grep -q '"notion"' "$mcp_file"; then
45
- fail "Prerequisito faltante: MCP Notion no disponible. Agrega la entrada 'notion' en $mcp_file y vuelve a ejecutar init-project."
46
- fi
47
- }
48
-
49
- require_env() {
50
- local var_name="$1"
51
- local var_value="${!var_name:-}"
52
-
53
- if [[ -z "$var_value" ]]; then
54
- fail "Variable requerida no definida: $var_name. Exporta $var_name y vuelve a ejecutar init-project."
55
- fi
56
- }
57
-
58
37
  require_command() {
59
38
  local command_name="$1"
60
39
  if ! command -v "$command_name" >/dev/null 2>&1; then
@@ -62,124 +41,212 @@ require_command() {
62
41
  fi
63
42
  }
64
43
 
65
- resolve_notion_token() {
66
- local mcp_file="$HOME/.config/opencode/mcp-servers.json"
67
- local token="${NOTION_TOKEN:-}"
68
- local py_bin
44
+ resolve_mcp_config_file() {
45
+ local opencode_file="$HOME/.config/opencode/opencode.json"
69
46
 
70
- if [[ -n "$token" ]]; then
71
- printf '%s' "$token"
47
+ if [[ -f "$opencode_file" ]]; then
48
+ printf '%s' "$opencode_file"
72
49
  return 0
73
50
  fi
74
51
 
52
+ fail "Prerequisito faltante: no existe $opencode_file. Configura OpenCode con MCP Notion habilitado y vuelve a ejecutar init-project."
53
+ }
54
+
55
+ validate_mcp_config() {
56
+ local mcp_file="$1"
57
+ local py_bin
58
+ local has_notion=""
59
+
75
60
  py_bin="$(pick_python || true)"
76
61
  if [[ -n "$py_bin" ]]; then
77
- token="$($py_bin - "$mcp_file" <<'PY'
62
+ has_notion="$($py_bin - "$mcp_file" <<'PY'
78
63
  import json
79
- import os
80
- import re
81
64
  import sys
82
65
 
83
- with open(sys.argv[1], "r", encoding="utf-8") as f:
84
- data = json.load(f)
85
-
86
- value = ""
87
- servers = data.get("servers", {})
88
- notion = servers.get("notion", {}) if isinstance(servers, dict) else {}
89
- env = notion.get("env", {}) if isinstance(notion, dict) else {}
90
-
91
- if isinstance(env, dict):
92
- raw = env.get("NOTION_TOKEN", "")
93
- if isinstance(raw, str):
94
- match = re.fullmatch(r"\$\{([^}]+)\}", raw.strip())
95
- if match:
96
- value = os.environ.get(match.group(1), "")
97
- else:
98
- value = raw
99
-
100
- print(value)
101
- PY
102
- )"
103
- fi
66
+ def has_enabled_notion(data):
67
+ if not isinstance(data, dict):
68
+ return False
104
69
 
105
- if [[ -z "$token" ]]; then
106
- fail "No se pudo resolver credencial de Notion. Define NOTION_TOKEN o configura servers.notion.env.NOTION_TOKEN en $mcp_file."
107
- fi
70
+ mcp = data.get("mcp")
71
+ if isinstance(mcp, dict):
72
+ notion = mcp.get("notion")
73
+ if notion not in (None, False):
74
+ if not (isinstance(notion, dict) and notion.get("enabled") is False):
75
+ return True
108
76
 
109
- printf '%s' "$token"
110
- }
77
+ servers = mcp.get("servers")
78
+ if isinstance(servers, dict) and "notion" in servers:
79
+ notion_server = servers.get("notion")
80
+ if not (isinstance(notion_server, dict) and notion_server.get("enabled") is False):
81
+ return True
111
82
 
112
- build_page_payload() {
113
- local parent_mode="$1"
114
- local parent_value="$2"
115
- local title="$3"
83
+ servers = data.get("servers")
84
+ if isinstance(servers, dict) and "notion" in servers:
85
+ notion_server = servers.get("notion")
86
+ if not (isinstance(notion_server, dict) and notion_server.get("enabled") is False):
87
+ return True
116
88
 
117
- if [[ "$parent_mode" == "page" ]]; then
118
- printf '{"parent":{"page_id":"%s"},"properties":{"title":{"title":[{"type":"text","text":{"content":"%s"}}]}}}' \
119
- "$parent_value" \
120
- "$(json_escape "$title")"
121
- return 0
89
+ return False
90
+
91
+ with open(sys.argv[1], "r", encoding="utf-8") as f:
92
+ raw = json.load(f)
93
+
94
+ print("yes" if has_enabled_notion(raw) else "no")
95
+ PY
96
+ )"
97
+ else
98
+ has_notion="$(grep -q '"notion"' "$mcp_file" && echo yes || echo no)"
122
99
  fi
123
100
 
124
- if [[ "$parent_mode" == "workspace" ]]; then
125
- printf '{"parent":{"workspace":true},"properties":{"title":{"title":[{"type":"text","text":{"content":"%s"}}]}}}' \
126
- "$(json_escape "$title")"
127
- return 0
101
+ if [[ "$has_notion" != "yes" ]]; then
102
+ fail "Prerequisito faltante: MCP Notion no habilitado en $mcp_file. Agrega la entrada mcp.notion (o mcp.servers.notion) y vuelve a ejecutar init-project."
128
103
  fi
104
+ }
129
105
 
130
- fail "Modo de parent no soportado para Notion payload: $parent_mode"
106
+ build_instruction_file() {
107
+ local instruction_file="$1"
108
+ local root_title="$2"
109
+ local parent_page_id="$3"
110
+
111
+ cat > "$instruction_file" <<EOF
112
+ Objetivo: crear la estructura base de Notion para un proyecto rootkid0-initializer usando UNICAMENTE herramientas MCP de Notion.
113
+
114
+ Reglas obligatorias:
115
+ - No usar API HTTP directa de Notion.
116
+ - No usar tokens o variables de entorno de Notion.
117
+ - Usar solo las herramientas MCP de Notion disponibles en esta sesion.
118
+ - Responder al final SOLO con un JSON valido, sin markdown y sin texto adicional.
119
+
120
+ Pasos a ejecutar:
121
+ 1) Crear una pagina raiz con titulo exacto: "$root_title".
122
+ 2) Si se provee parent_page_id y no esta vacio, crear la pagina raiz como hija de ese page_id.
123
+ 3) Crear bajo la pagina raiz estas paginas de fase:
124
+ - 01-business
125
+ - 02-proposal
126
+ - 03-design
127
+ - 04-management
128
+ - 05-development
129
+ - 06-deployment
130
+ - 07-production
131
+ - 99-common
132
+ 4) Dentro de 99-common crear estas secciones:
133
+ - Projects
134
+ - Phases
135
+ - Deliverables
136
+ - Backlog
137
+ - Risks
138
+ - Decisions
139
+ - Incidents
140
+
141
+ Datos de entrada:
142
+ - root_title: "$root_title"
143
+ - parent_page_id: "$parent_page_id"
144
+
145
+ Formato de salida requerido (JSON exacto en estructura):
146
+ {
147
+ "notion_parent_mode": "workspace o page",
148
+ "notion_parent_page_id": "id o vacio",
149
+ "project_root_page_id": "id",
150
+ "phase_pages": {
151
+ "01-business": "id",
152
+ "02-proposal": "id",
153
+ "03-design": "id",
154
+ "04-management": "id",
155
+ "05-development": "id",
156
+ "06-deployment": "id",
157
+ "07-production": "id",
158
+ "99-common": "id"
159
+ },
160
+ "model_sections": {
161
+ "Projects": "id",
162
+ "Phases": "id",
163
+ "Deliverables": "id",
164
+ "Backlog": "id",
165
+ "Risks": "id",
166
+ "Decisions": "id",
167
+ "Incidents": "id"
168
+ }
169
+ }
170
+ EOF
131
171
  }
132
172
 
133
- create_notion_page() {
134
- local parent_mode="$1"
135
- local parent_value="$2"
136
- local title="$3"
137
- local payload
138
- local response_file
139
- local status
173
+ extract_bootstrap_result() {
174
+ local events_file="$1"
140
175
  local py_bin
141
- local page_id
142
-
143
- payload="$(build_page_payload "$parent_mode" "$parent_value" "$title")"
144
- response_file="$(mktemp)"
145
-
146
- status="$(curl -sS -o "$response_file" -w "%{http_code}" \
147
- -X POST "https://api.notion.com/v1/pages" \
148
- -H "Authorization: Bearer $NOTION_AUTH_TOKEN" \
149
- -H "Notion-Version: 2022-06-28" \
150
- -H "Content-Type: application/json" \
151
- --data "$payload")"
152
-
153
- if [[ "$status" -lt 200 || "$status" -ge 300 ]]; then
154
- local error_body
155
- error_body="$(tr -d '\n' < "$response_file")"
156
- rm -f "$response_file"
157
- fail "Notion API devolvio HTTP $status al crear '$title'. Respuesta: $error_body"
176
+ py_bin="$(pick_python || true)"
177
+
178
+ if [[ -z "$py_bin" ]]; then
179
+ fail "No se encontro python/python3 para validar salida JSON de OpenCode."
158
180
  fi
159
181
 
160
- py_bin="$(pick_python || true)"
161
- if [[ -n "$py_bin" ]]; then
162
- page_id="$("$py_bin" - "$response_file" <<'PY'
182
+ "$py_bin" - "$events_file" <<'PY'
163
183
  import json
184
+ import re
164
185
  import sys
165
186
 
166
- with open(sys.argv[1], "r", encoding="utf-8") as f:
167
- data = json.load(f)
168
-
169
- print(data.get("id", ""))
187
+ events_file = sys.argv[1]
188
+ parts = []
189
+
190
+ with open(events_file, "r", encoding="utf-8") as f:
191
+ for line in f:
192
+ line = line.strip()
193
+ if not line:
194
+ continue
195
+ try:
196
+ evt = json.loads(line)
197
+ except json.JSONDecodeError:
198
+ continue
199
+
200
+ if evt.get("type") == "text":
201
+ part = evt.get("part", {})
202
+ text = part.get("text")
203
+ if isinstance(text, str) and text.strip():
204
+ parts.append(text)
205
+
206
+ combined = "\n".join(parts).strip()
207
+ if not combined:
208
+ raise SystemExit("No se recibio respuesta de texto desde opencode run.")
209
+
210
+ fence_match = re.fullmatch(r"```(?:json)?\s*(.*?)\s*```", combined, flags=re.DOTALL)
211
+ if fence_match:
212
+ combined = fence_match.group(1).strip()
213
+
214
+ data = json.loads(combined)
215
+
216
+ required_phases = [
217
+ "01-business",
218
+ "02-proposal",
219
+ "03-design",
220
+ "04-management",
221
+ "05-development",
222
+ "06-deployment",
223
+ "07-production",
224
+ "99-common",
225
+ ]
226
+ required_sections = [
227
+ "Projects",
228
+ "Phases",
229
+ "Deliverables",
230
+ "Backlog",
231
+ "Risks",
232
+ "Decisions",
233
+ "Incidents",
234
+ ]
235
+
236
+ for top in ["project_root_page_id", "phase_pages", "model_sections"]:
237
+ if top not in data:
238
+ raise SystemExit(f"Falta campo obligatorio en respuesta MCP: {top}")
239
+
240
+ for key in required_phases:
241
+ if key not in data["phase_pages"]:
242
+ raise SystemExit(f"Falta fase obligatoria en respuesta MCP: {key}")
243
+
244
+ for key in required_sections:
245
+ if key not in data["model_sections"]:
246
+ raise SystemExit(f"Falta seccion obligatoria en respuesta MCP: {key}")
247
+
248
+ print(json.dumps(data, ensure_ascii=False))
170
249
  PY
171
- )"
172
- else
173
- page_id="$(grep -o '"id":"[^"]*"' "$response_file" | head -n 1 | cut -d '"' -f 4)"
174
- fi
175
-
176
- rm -f "$response_file"
177
-
178
- if [[ -z "$page_id" ]]; then
179
- fail "No se pudo extraer el id de la pagina creada para '$title'."
180
- fi
181
-
182
- printf '%s' "$page_id"
183
250
  }
184
251
 
185
252
  PROJECT_NAME=""
@@ -214,14 +281,14 @@ if [[ ! -d "$PROJECT_DIR" ]]; then
214
281
  fail "No existe el directorio de proyecto: $PROJECT_DIR"
215
282
  fi
216
283
 
217
- validate_mcp_config
218
- require_command "curl"
219
- NOTION_AUTH_TOKEN="$(resolve_notion_token)"
284
+ require_command "opencode"
285
+ mcp_config_file="$(resolve_mcp_config_file)"
286
+ validate_mcp_config "$mcp_config_file"
220
287
 
221
288
  NOTION_WORKSPACE_NAME="${NOTION_WORKSPACE_NAME:-}"
222
289
  NOTION_PARENT_PAGE_ID="${NOTION_PARENT_PAGE_ID:-}"
223
290
 
224
- echo "Iniciando bootstrap automatico de Notion..."
291
+ echo "Iniciando bootstrap automatico de Notion via MCP..."
225
292
 
226
293
  root_title="$PROJECT_NAME"
227
294
  if [[ -n "$NOTION_WORKSPACE_NAME" ]]; then
@@ -229,102 +296,61 @@ if [[ -n "$NOTION_WORKSPACE_NAME" ]]; then
229
296
  fi
230
297
 
231
298
  parent_mode="workspace"
232
- parent_value=""
233
-
234
299
  if [[ -n "$NOTION_PARENT_PAGE_ID" ]]; then
235
300
  parent_mode="page"
236
- parent_value="$NOTION_PARENT_PAGE_ID"
237
- echo "Modo parent Notion: page ($NOTION_PARENT_PAGE_ID)"
301
+ echo "Modo parent Notion (opcional): page ($NOTION_PARENT_PAGE_ID)"
238
302
  else
239
- echo "Modo parent Notion: workspace (NOTION_PARENT_PAGE_ID no definido)"
303
+ echo "Modo parent Notion (opcional): workspace"
240
304
  fi
241
305
 
242
- project_root_page_id="$(create_notion_page "$parent_mode" "$parent_value" "$root_title")"
243
- echo "Pagina raiz creada: $project_root_page_id"
244
-
245
- phase_names=(
246
- "01-business"
247
- "02-proposal"
248
- "03-design"
249
- "04-management"
250
- "05-development"
251
- "06-deployment"
252
- "07-production"
253
- "99-common"
254
- )
255
-
256
- phase_ids=()
257
- common_page_id=""
258
-
259
- for phase in "${phase_names[@]}"; do
260
- phase_id="$(create_notion_page "page" "$project_root_page_id" "$phase")"
261
- phase_ids+=("$phase_id")
262
- echo "Pagina creada ($phase): $phase_id"
263
- if [[ "$phase" == "99-common" ]]; then
264
- common_page_id="$phase_id"
265
- fi
266
- done
306
+ instruction_file="$(mktemp)"
307
+ events_file="$(mktemp)"
308
+ trap 'rm -f "$instruction_file" "$events_file"' EXIT
267
309
 
268
- if [[ -z "$common_page_id" ]]; then
269
- fail "No se pudo crear la pagina 99-common en Notion."
310
+ build_instruction_file "$instruction_file" "$root_title" "$NOTION_PARENT_PAGE_ID"
311
+
312
+ if ! opencode run --format json --dir "$PROJECT_DIR" --file "$instruction_file" \
313
+ "Sigue las instrucciones del archivo adjunto y devuelve SOLO el JSON final." > "$events_file"; then
314
+ fail "Fallo la ejecucion de OpenCode para bootstrap de Notion. Verifica que MCP Notion este disponible y operativo."
270
315
  fi
271
316
 
272
- section_names=(
273
- "Projects"
274
- "Phases"
275
- "Deliverables"
276
- "Backlog"
277
- "Risks"
278
- "Decisions"
279
- "Incidents"
280
- )
281
-
282
- section_ids=()
283
- for section in "${section_names[@]}"; do
284
- section_id="$(create_notion_page "page" "$common_page_id" "$section")"
285
- section_ids+=("$section_id")
286
- echo "Seccion modelo MVP creada ($section): $section_id"
287
- done
317
+ mcp_result_json="$(extract_bootstrap_result "$events_file")"
288
318
 
289
319
  output_file="$PROJECT_DIR/99-common/notion-bootstrap.output.json"
290
320
  timestamp_utc="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
291
321
 
292
- {
293
- echo "{"
294
- echo " \"project_name\": \"$(json_escape "$PROJECT_NAME")\","
295
- echo " \"workspace_name\": \"$(json_escape "$NOTION_WORKSPACE_NAME")\","
296
- echo " \"created_at_utc\": \"$timestamp_utc\","
297
- echo " \"notion_parent_mode\": \"$parent_mode\","
298
- echo " \"notion_parent_page_id\": \"$(json_escape "$NOTION_PARENT_PAGE_ID")\","
299
- echo " \"project_root_page_id\": \"$project_root_page_id\","
300
- echo " \"phase_pages\": {"
301
-
302
- for i in "${!phase_names[@]}"; do
303
- key="${phase_names[$i]}"
304
- value="${phase_ids[$i]}"
305
- suffix=","
306
- if [[ "$i" -eq $((${#phase_names[@]} - 1)) ]]; then
307
- suffix=""
308
- fi
309
- echo " \"$(json_escape "$key")\": \"$value\"$suffix"
310
- done
311
-
312
- echo " },"
313
- echo " \"model_sections\": {"
314
-
315
- for i in "${!section_names[@]}"; do
316
- key="${section_names[$i]}"
317
- value="${section_ids[$i]}"
318
- suffix=","
319
- if [[ "$i" -eq $((${#section_names[@]} - 1)) ]]; then
320
- suffix=""
321
- fi
322
- echo " \"$(json_escape "$key")\": \"$value\"$suffix"
323
- done
324
-
325
- echo " }"
326
- echo "}"
327
- } > "$output_file"
328
-
329
- echo "Bootstrap Notion completado."
322
+ py_bin="$(pick_python || true)"
323
+ if [[ -z "$py_bin" ]]; then
324
+ fail "No se encontro python/python3 para consolidar salida JSON del bootstrap Notion."
325
+ fi
326
+
327
+ "$py_bin" - "$mcp_result_json" "$PROJECT_NAME" "$NOTION_WORKSPACE_NAME" "$timestamp_utc" "$parent_mode" "$NOTION_PARENT_PAGE_ID" "$output_file" <<'PY'
328
+ import json
329
+ import sys
330
+
331
+ mcp_result = json.loads(sys.argv[1])
332
+ project_name = sys.argv[2]
333
+ workspace_name = sys.argv[3]
334
+ timestamp_utc = sys.argv[4]
335
+ parent_mode = sys.argv[5]
336
+ parent_page_id = sys.argv[6]
337
+ output_file = sys.argv[7]
338
+
339
+ result = {
340
+ "project_name": project_name,
341
+ "workspace_name": workspace_name,
342
+ "created_at_utc": timestamp_utc,
343
+ "notion_parent_mode": mcp_result.get("notion_parent_mode", parent_mode),
344
+ "notion_parent_page_id": mcp_result.get("notion_parent_page_id", parent_page_id),
345
+ "project_root_page_id": mcp_result["project_root_page_id"],
346
+ "phase_pages": mcp_result["phase_pages"],
347
+ "model_sections": mcp_result["model_sections"],
348
+ }
349
+
350
+ with open(output_file, "w", encoding="utf-8") as f:
351
+ json.dump(result, f, indent=2, ensure_ascii=False)
352
+ f.write("\n")
353
+ PY
354
+
355
+ echo "Bootstrap Notion completado via MCP."
330
356
  echo "Salida guardada en: $output_file"