rootkid0-initializer 0.1.2 → 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,14 +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/opencode.json` (preferido)
30
- - `~/.config/opencode/mcp-servers.json` (legacy)
31
+ - `~/.config/opencode/opencode.json`
31
32
 
32
33
  Condicion minima requerida:
33
34
 
34
- - Debe existir entrada `notion` en el archivo global usado.
35
+ - Debe existir `mcp.notion` habilitado (tambien soporta `mcp.servers.notion`).
35
36
 
36
37
  Si falta, el init falla con mensaje de correccion porque el setup Notion es automatico en P1.
37
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/opencode.json` (preferido) o `~/.config/opencode/mcp-servers.json` (legacy), 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,6 +1,6 @@
1
1
  {
2
2
  "name": "rootkid0-initializer",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI para inicializar el flujo de proyectos al estilo rootkid0.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -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,161 +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
- if ($null -ne $raw.servers) {
32
- $fromConfig = $raw.servers.notion.env.NOTION_TOKEN
33
- }
34
- elseif ($null -ne $raw.mcp -and $null -ne $raw.mcp.servers) {
35
- $fromConfig = $raw.mcp.servers.notion.env.NOTION_TOKEN
36
- }
37
- else {
38
- $fromConfig = ""
39
- }
40
35
  }
41
36
  catch {
42
- $fromConfig = ""
37
+ return $false
43
38
  }
44
39
 
45
- if ($fromConfig -is [string]) {
46
- if ($fromConfig -match '^\$\{(.+)\}$') {
47
- $refVar = $Matches[1]
48
- $resolved = [Environment]::GetEnvironmentVariable($refVar)
49
- if (-not [string]::IsNullOrWhiteSpace($resolved)) {
50
- 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
51
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
52
49
  }
53
- elseif (-not [string]::IsNullOrWhiteSpace($fromConfig)) {
54
- return $fromConfig
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
54
+ }
55
+ return $true
55
56
  }
56
57
  }
57
58
 
58
- Fail "No se pudo resolver credencial de Notion. Define NOTION_TOKEN o configura servers.notion.env.NOTION_TOKEN en $McpFile."
59
- }
60
-
61
- function Resolve-McpConfigFile() {
62
- $opencodeFile = Join-Path $HOME ".config/opencode/opencode.json"
63
- $legacyFile = Join-Path $HOME ".config/opencode/mcp-servers.json"
64
-
65
- if (Test-Path -LiteralPath $opencodeFile) {
66
- return $opencodeFile
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
62
+ }
63
+ return $true
67
64
  }
68
65
 
69
- if (Test-Path -LiteralPath $legacyFile) {
70
- return $legacyFile
66
+ return $false
67
+ }
68
+
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"
71
127
  }
128
+ }
129
+ "@
72
130
 
73
- Fail "Prerequisito faltante: MCP Notion no disponible. Debe existir $opencodeFile (preferido) o $legacyFile con entrada notion antes de ejecutar init-project."
131
+ Set-Content -Path $Path -Value $content
74
132
  }
75
133
 
76
- function Has-NotionServer([string]$McpFile) {
77
- try {
78
- $raw = Get-Content -Path $McpFile -Raw | ConvertFrom-Json
79
- if ($null -ne $raw.servers -and $null -ne $raw.servers.notion) {
80
- return $true
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
81
141
  }
82
- if ($null -ne $raw.mcp -and $null -ne $raw.mcp.servers -and $null -ne $raw.mcp.servers.notion) {
83
- return $true
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)
84
152
  }
85
- return $false
86
153
  }
87
- catch {
88
- return $false
154
+
155
+ if ($textParts.Count -eq 0) {
156
+ Fail "No se recibio respuesta de texto desde opencode run."
89
157
  }
90
- }
91
158
 
92
- function Get-NotionPagePayload([string]$ParentMode, [string]$ParentValue, [string]$Title) {
93
- if ($ParentMode -eq "page") {
94
- return @{
95
- parent = @{ page_id = $ParentValue }
96
- properties = @{
97
- title = @{
98
- title = @(
99
- @{
100
- type = "text"
101
- text = @{ content = $Title }
102
- }
103
- )
104
- }
105
- }
106
- } | ConvertTo-Json -Depth 10
107
- }
108
-
109
- if ($ParentMode -eq "workspace") {
110
- return @{
111
- parent = @{ workspace = $true }
112
- properties = @{
113
- title = @{
114
- title = @(
115
- @{
116
- type = "text"
117
- text = @{ content = $Title }
118
- }
119
- )
120
- }
121
- }
122
- } | ConvertTo-Json -Depth 10
159
+ $combined = ($textParts -join "`n").Trim()
160
+ if ($combined -match '^```(?:json)?\s*([\s\S]*?)\s*```$') {
161
+ $combined = $Matches[1].Trim()
123
162
  }
124
163
 
125
- Fail "Modo de parent no soportado para Notion payload: $ParentMode"
126
- }
164
+ try {
165
+ $result = $combined | ConvertFrom-Json
166
+ }
167
+ catch {
168
+ Fail "OpenCode no devolvio JSON valido para bootstrap Notion."
169
+ }
127
170
 
128
- function New-NotionPage([string]$ParentMode, [string]$ParentValue, [string]$Title) {
129
- $headers = @{
130
- Authorization = "Bearer $script:NotionToken"
131
- "Notion-Version" = "2022-06-28"
132
- "Content-Type" = "application/json"
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"
133
194
  }
134
195
 
135
- $body = Get-NotionPagePayload -ParentMode $ParentMode -ParentValue $ParentValue -Title $Title
196
+ if ($null -eq $result.phase_pages) {
197
+ Fail "Falta campo obligatorio en respuesta MCP: phase_pages"
198
+ }
136
199
 
137
- try {
138
- $response = Invoke-RestMethod -Method Post -Uri "https://api.notion.com/v1/pages" -Headers $headers -Body $body
200
+ if ($null -eq $result.model_sections) {
201
+ Fail "Falta campo obligatorio en respuesta MCP: model_sections"
139
202
  }
140
- catch {
141
- $apiMessage = $_.ErrorDetails.Message
142
- if ([string]::IsNullOrWhiteSpace($apiMessage)) {
143
- $apiMessage = $_.Exception.Message
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"
144
208
  }
145
- Fail "No se pudo crear pagina en Notion ($Title). Detalle: $apiMessage"
146
209
  }
147
210
 
148
- if ([string]::IsNullOrWhiteSpace($response.id)) {
149
- 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
+ }
150
216
  }
151
217
 
152
- return $response.id
218
+ return $result
153
219
  }
154
220
 
155
221
  if (-not (Test-Path -LiteralPath $ProjectDir)) {
156
222
  Fail "No existe el directorio de proyecto: $ProjectDir"
157
223
  }
158
224
 
159
- $mcpFile = Resolve-McpConfigFile
225
+ Require-Command "opencode"
160
226
 
161
- if (-not (Has-NotionServer -McpFile $mcpFile)) {
162
- 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."
163
230
  }
164
231
 
165
- $script:NotionToken = Resolve-NotionToken -McpFile $mcpFile
166
-
167
232
  $workspaceName = [Environment]::GetEnvironmentVariable("NOTION_WORKSPACE_NAME")
168
233
  $parentPageId = [Environment]::GetEnvironmentVariable("NOTION_PARENT_PAGE_ID")
169
234
 
170
- Write-Host "Iniciando bootstrap automatico de Notion..."
235
+ Write-Host "Iniciando bootstrap automatico de Notion via MCP..."
171
236
 
172
237
  $rootTitle = $ProjectName
173
238
  if (-not [string]::IsNullOrWhiteSpace($workspaceName)) {
@@ -175,79 +240,50 @@ if (-not [string]::IsNullOrWhiteSpace($workspaceName)) {
175
240
  }
176
241
 
177
242
  $parentMode = "workspace"
178
- $parentValue = ""
179
-
180
243
  if (-not [string]::IsNullOrWhiteSpace($parentPageId)) {
181
244
  $parentMode = "page"
182
- $parentValue = $parentPageId
183
- Write-Host "Modo parent Notion: page ($parentPageId)"
245
+ Write-Host "Modo parent Notion (opcional): page ($parentPageId)"
184
246
  }
185
247
  else {
186
- Write-Host "Modo parent Notion: workspace (NOTION_PARENT_PAGE_ID no definido)"
248
+ Write-Host "Modo parent Notion (opcional): workspace"
187
249
  }
188
250
 
189
- $projectRootPageId = New-NotionPage -ParentMode $parentMode -ParentValue $parentValue -Title $rootTitle
190
- Write-Host "Pagina raiz creada: $projectRootPageId"
191
-
192
- $phaseNames = @(
193
- "01-business",
194
- "02-proposal",
195
- "03-design",
196
- "04-management",
197
- "05-development",
198
- "06-deployment",
199
- "07-production",
200
- "99-common"
201
- )
251
+ $instructionFile = [System.IO.Path]::GetTempFileName()
252
+ $eventsFile = [System.IO.Path]::GetTempFileName()
202
253
 
203
- $phaseMap = [ordered]@{}
204
- $commonPageId = ""
254
+ try {
255
+ New-InstructionFile -Path $instructionFile -RootTitle $rootTitle -ParentPageId $parentPageId
205
256
 
206
- foreach ($phase in $phaseNames) {
207
- $phaseId = New-NotionPage -ParentMode "page" -ParentValue $projectRootPageId -Title $phase
208
- $phaseMap[$phase] = $phaseId
209
- Write-Host "Pagina creada ($phase): $phaseId"
210
- if ($phase -eq "99-common") {
211
- $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."
212
260
  }
213
- }
214
261
 
215
- if ([string]::IsNullOrWhiteSpace($commonPageId)) {
216
- Fail "No se pudo crear la pagina 99-common en Notion."
217
- }
262
+ $mcpResult = Parse-OpenCodeEvents -EventsPath $eventsFile
218
263
 
219
- $sectionNames = @(
220
- "Projects",
221
- "Phases",
222
- "Deliverables",
223
- "Backlog",
224
- "Risks",
225
- "Decisions",
226
- "Incidents"
227
- )
264
+ $outputPath = Join-Path $ProjectDir "99-common/notion-bootstrap.output.json"
228
265
 
229
- $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
+ }
230
276
 
231
- foreach ($section in $sectionNames) {
232
- $sectionId = New-NotionPage -ParentMode "page" -ParentValue $commonPageId -Title $section
233
- $sectionMap[$section] = $sectionId
234
- Write-Host "Seccion modelo MVP creada ($section): $sectionId"
235
- }
277
+ $result | ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath
236
278
 
237
- $outputPath = Join-Path $ProjectDir "99-common/notion-bootstrap.output.json"
238
-
239
- $result = [ordered]@{
240
- project_name = $ProjectName
241
- workspace_name = $workspaceName
242
- created_at_utc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
243
- notion_parent_mode = $parentMode
244
- notion_parent_page_id = $parentPageId
245
- project_root_page_id = $projectRootPageId
246
- phase_pages = $phaseMap
247
- 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
+ }
248
289
  }
249
-
250
- $result | ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath
251
-
252
- Write-Host "Bootstrap Notion completado."
253
- Write-Host "Salida guardada en: $outputPath"
@@ -34,52 +34,6 @@ pick_python() {
34
34
  return 1
35
35
  }
36
36
 
37
- validate_mcp_config() {
38
- local mcp_file="$1"
39
- local py_bin
40
- local has_notion=""
41
-
42
- if [[ ! -f "$mcp_file" ]]; then
43
- fail "Prerequisito faltante: MCP Notion no disponible. Debe existir $mcp_file con entrada notion antes de ejecutar init-project."
44
- fi
45
-
46
- py_bin="$(pick_python || true)"
47
- if [[ -n "$py_bin" ]]; then
48
- has_notion="$($py_bin - "$mcp_file" <<'PY'
49
- import json
50
- import sys
51
-
52
- with open(sys.argv[1], "r", encoding="utf-8") as f:
53
- data = json.load(f)
54
-
55
- servers = {}
56
- if isinstance(data, dict):
57
- if isinstance(data.get("servers"), dict):
58
- servers = data.get("servers", {})
59
- elif isinstance(data.get("mcp"), dict) and isinstance(data["mcp"].get("servers"), dict):
60
- servers = data["mcp"]["servers"]
61
-
62
- print("yes" if "notion" in servers else "no")
63
- PY
64
- )"
65
- else
66
- has_notion="$(grep -q '"notion"' "$mcp_file" && echo yes || echo no)"
67
- fi
68
-
69
- if [[ "$has_notion" != "yes" ]]; then
70
- fail "Prerequisito faltante: MCP Notion no disponible. Agrega la entrada notion en $mcp_file y vuelve a ejecutar init-project."
71
- fi
72
- }
73
-
74
- require_env() {
75
- local var_name="$1"
76
- local var_value="${!var_name:-}"
77
-
78
- if [[ -z "$var_value" ]]; then
79
- fail "Variable requerida no definida: $var_name. Exporta $var_name y vuelve a ejecutar init-project."
80
- fi
81
- }
82
-
83
37
  require_command() {
84
38
  local command_name="$1"
85
39
  if ! command -v "$command_name" >/dev/null 2>&1; then
@@ -87,147 +41,212 @@ require_command() {
87
41
  fi
88
42
  }
89
43
 
90
- resolve_notion_token() {
91
- local mcp_file="$1"
92
- local token="${NOTION_TOKEN:-}"
93
- local py_bin
44
+ resolve_mcp_config_file() {
45
+ local opencode_file="$HOME/.config/opencode/opencode.json"
94
46
 
95
- if [[ -n "$token" ]]; then
96
- printf '%s' "$token"
47
+ if [[ -f "$opencode_file" ]]; then
48
+ printf '%s' "$opencode_file"
97
49
  return 0
98
50
  fi
99
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
+
100
60
  py_bin="$(pick_python || true)"
101
61
  if [[ -n "$py_bin" ]]; then
102
- token="$($py_bin - "$mcp_file" <<'PY'
62
+ has_notion="$($py_bin - "$mcp_file" <<'PY'
103
63
  import json
104
- import os
105
- import re
106
64
  import sys
107
65
 
108
- with open(sys.argv[1], "r", encoding="utf-8") as f:
109
- data = json.load(f)
110
-
111
- value = ""
112
- servers = {}
113
- if isinstance(data, dict):
114
- if isinstance(data.get("servers"), dict):
115
- servers = data.get("servers", {})
116
- elif isinstance(data.get("mcp"), dict) and isinstance(data["mcp"].get("servers"), dict):
117
- servers = data["mcp"]["servers"]
118
-
119
- notion = servers.get("notion", {}) if isinstance(servers, dict) else {}
120
- env = notion.get("env", {}) if isinstance(notion, dict) else {}
121
-
122
- if isinstance(env, dict):
123
- raw = env.get("NOTION_TOKEN", "")
124
- if isinstance(raw, str):
125
- match = re.fullmatch(r"\$\{([^}]+)\}", raw.strip())
126
- if match:
127
- value = os.environ.get(match.group(1), "")
128
- else:
129
- value = raw
130
-
131
- print(value)
132
- PY
133
- )"
134
- fi
135
-
136
- if [[ -z "$token" ]]; then
137
- fail "No se pudo resolver credencial de Notion. Define NOTION_TOKEN o configura servers.notion.env.NOTION_TOKEN en $mcp_file."
138
- fi
139
-
140
- printf '%s' "$token"
141
- }
66
+ def has_enabled_notion(data):
67
+ if not isinstance(data, dict):
68
+ return False
142
69
 
143
- resolve_mcp_config_file() {
144
- local opencode_file="$HOME/.config/opencode/opencode.json"
145
- local legacy_file="$HOME/.config/opencode/mcp-servers.json"
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
146
76
 
147
- if [[ -f "$opencode_file" ]]; then
148
- printf '%s' "$opencode_file"
149
- return 0
150
- fi
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
151
82
 
152
- if [[ -f "$legacy_file" ]]; then
153
- printf '%s' "$legacy_file"
154
- return 0
155
- fi
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
156
88
 
157
- fail "Prerequisito faltante: MCP Notion no disponible. Debe existir $opencode_file (preferido) o $legacy_file con entrada notion antes de ejecutar init-project."
158
- }
89
+ return False
159
90
 
160
- build_page_payload() {
161
- local parent_mode="$1"
162
- local parent_value="$2"
163
- local title="$3"
91
+ with open(sys.argv[1], "r", encoding="utf-8") as f:
92
+ raw = json.load(f)
164
93
 
165
- if [[ "$parent_mode" == "page" ]]; then
166
- printf '{"parent":{"page_id":"%s"},"properties":{"title":{"title":[{"type":"text","text":{"content":"%s"}}]}}}' \
167
- "$parent_value" \
168
- "$(json_escape "$title")"
169
- return 0
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)"
170
99
  fi
171
100
 
172
- if [[ "$parent_mode" == "workspace" ]]; then
173
- printf '{"parent":{"workspace":true},"properties":{"title":{"title":[{"type":"text","text":{"content":"%s"}}]}}}' \
174
- "$(json_escape "$title")"
175
- 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."
176
103
  fi
104
+ }
177
105
 
178
- 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
179
171
  }
180
172
 
181
- create_notion_page() {
182
- local parent_mode="$1"
183
- local parent_value="$2"
184
- local title="$3"
185
- local payload
186
- local response_file
187
- local status
173
+ extract_bootstrap_result() {
174
+ local events_file="$1"
188
175
  local py_bin
189
- local page_id
190
-
191
- payload="$(build_page_payload "$parent_mode" "$parent_value" "$title")"
192
- response_file="$(mktemp)"
193
-
194
- status="$(curl -sS -o "$response_file" -w "%{http_code}" \
195
- -X POST "https://api.notion.com/v1/pages" \
196
- -H "Authorization: Bearer $NOTION_AUTH_TOKEN" \
197
- -H "Notion-Version: 2022-06-28" \
198
- -H "Content-Type: application/json" \
199
- --data "$payload")"
200
-
201
- if [[ "$status" -lt 200 || "$status" -ge 300 ]]; then
202
- local error_body
203
- error_body="$(tr -d '\n' < "$response_file")"
204
- rm -f "$response_file"
205
- 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."
206
180
  fi
207
181
 
208
- py_bin="$(pick_python || true)"
209
- if [[ -n "$py_bin" ]]; then
210
- page_id="$("$py_bin" - "$response_file" <<'PY'
182
+ "$py_bin" - "$events_file" <<'PY'
211
183
  import json
184
+ import re
212
185
  import sys
213
186
 
214
- with open(sys.argv[1], "r", encoding="utf-8") as f:
215
- data = json.load(f)
216
-
217
- 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))
218
249
  PY
219
- )"
220
- else
221
- page_id="$(grep -o '"id":"[^"]*"' "$response_file" | head -n 1 | cut -d '"' -f 4)"
222
- fi
223
-
224
- rm -f "$response_file"
225
-
226
- if [[ -z "$page_id" ]]; then
227
- fail "No se pudo extraer el id de la pagina creada para '$title'."
228
- fi
229
-
230
- printf '%s' "$page_id"
231
250
  }
232
251
 
233
252
  PROJECT_NAME=""
@@ -262,15 +281,14 @@ if [[ ! -d "$PROJECT_DIR" ]]; then
262
281
  fail "No existe el directorio de proyecto: $PROJECT_DIR"
263
282
  fi
264
283
 
284
+ require_command "opencode"
265
285
  mcp_config_file="$(resolve_mcp_config_file)"
266
286
  validate_mcp_config "$mcp_config_file"
267
- require_command "curl"
268
- NOTION_AUTH_TOKEN="$(resolve_notion_token "$mcp_config_file")"
269
287
 
270
288
  NOTION_WORKSPACE_NAME="${NOTION_WORKSPACE_NAME:-}"
271
289
  NOTION_PARENT_PAGE_ID="${NOTION_PARENT_PAGE_ID:-}"
272
290
 
273
- echo "Iniciando bootstrap automatico de Notion..."
291
+ echo "Iniciando bootstrap automatico de Notion via MCP..."
274
292
 
275
293
  root_title="$PROJECT_NAME"
276
294
  if [[ -n "$NOTION_WORKSPACE_NAME" ]]; then
@@ -278,102 +296,61 @@ if [[ -n "$NOTION_WORKSPACE_NAME" ]]; then
278
296
  fi
279
297
 
280
298
  parent_mode="workspace"
281
- parent_value=""
282
-
283
299
  if [[ -n "$NOTION_PARENT_PAGE_ID" ]]; then
284
300
  parent_mode="page"
285
- parent_value="$NOTION_PARENT_PAGE_ID"
286
- echo "Modo parent Notion: page ($NOTION_PARENT_PAGE_ID)"
301
+ echo "Modo parent Notion (opcional): page ($NOTION_PARENT_PAGE_ID)"
287
302
  else
288
- echo "Modo parent Notion: workspace (NOTION_PARENT_PAGE_ID no definido)"
303
+ echo "Modo parent Notion (opcional): workspace"
289
304
  fi
290
305
 
291
- project_root_page_id="$(create_notion_page "$parent_mode" "$parent_value" "$root_title")"
292
- echo "Pagina raiz creada: $project_root_page_id"
293
-
294
- phase_names=(
295
- "01-business"
296
- "02-proposal"
297
- "03-design"
298
- "04-management"
299
- "05-development"
300
- "06-deployment"
301
- "07-production"
302
- "99-common"
303
- )
304
-
305
- phase_ids=()
306
- common_page_id=""
307
-
308
- for phase in "${phase_names[@]}"; do
309
- phase_id="$(create_notion_page "page" "$project_root_page_id" "$phase")"
310
- phase_ids+=("$phase_id")
311
- echo "Pagina creada ($phase): $phase_id"
312
- if [[ "$phase" == "99-common" ]]; then
313
- common_page_id="$phase_id"
314
- fi
315
- done
306
+ instruction_file="$(mktemp)"
307
+ events_file="$(mktemp)"
308
+ trap 'rm -f "$instruction_file" "$events_file"' EXIT
316
309
 
317
- if [[ -z "$common_page_id" ]]; then
318
- 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."
319
315
  fi
320
316
 
321
- section_names=(
322
- "Projects"
323
- "Phases"
324
- "Deliverables"
325
- "Backlog"
326
- "Risks"
327
- "Decisions"
328
- "Incidents"
329
- )
330
-
331
- section_ids=()
332
- for section in "${section_names[@]}"; do
333
- section_id="$(create_notion_page "page" "$common_page_id" "$section")"
334
- section_ids+=("$section_id")
335
- echo "Seccion modelo MVP creada ($section): $section_id"
336
- done
317
+ mcp_result_json="$(extract_bootstrap_result "$events_file")"
337
318
 
338
319
  output_file="$PROJECT_DIR/99-common/notion-bootstrap.output.json"
339
320
  timestamp_utc="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
340
321
 
341
- {
342
- echo "{"
343
- echo " \"project_name\": \"$(json_escape "$PROJECT_NAME")\","
344
- echo " \"workspace_name\": \"$(json_escape "$NOTION_WORKSPACE_NAME")\","
345
- echo " \"created_at_utc\": \"$timestamp_utc\","
346
- echo " \"notion_parent_mode\": \"$parent_mode\","
347
- echo " \"notion_parent_page_id\": \"$(json_escape "$NOTION_PARENT_PAGE_ID")\","
348
- echo " \"project_root_page_id\": \"$project_root_page_id\","
349
- echo " \"phase_pages\": {"
350
-
351
- for i in "${!phase_names[@]}"; do
352
- key="${phase_names[$i]}"
353
- value="${phase_ids[$i]}"
354
- suffix=","
355
- if [[ "$i" -eq $((${#phase_names[@]} - 1)) ]]; then
356
- suffix=""
357
- fi
358
- echo " \"$(json_escape "$key")\": \"$value\"$suffix"
359
- done
360
-
361
- echo " },"
362
- echo " \"model_sections\": {"
363
-
364
- for i in "${!section_names[@]}"; do
365
- key="${section_names[$i]}"
366
- value="${section_ids[$i]}"
367
- suffix=","
368
- if [[ "$i" -eq $((${#section_names[@]} - 1)) ]]; then
369
- suffix=""
370
- fi
371
- echo " \"$(json_escape "$key")\": \"$value\"$suffix"
372
- done
373
-
374
- echo " }"
375
- echo "}"
376
- } > "$output_file"
377
-
378
- 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."
379
356
  echo "Salida guardada en: $output_file"