openprompt-lang 1.2.4 → 1.2.7

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.
@@ -135,18 +135,23 @@ export async function initExisting(options = {}) {
135
135
  ? JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8")).name || "mi-proyecto"
136
136
  : "mi-proyecto"
137
137
 
138
+ const interactive = options.interactive !== false && !!process.stdin.isTTY
138
139
  if (!rebuild) {
139
140
  console.log(chalk.cyan(`\n🔧 Inicializando OPL en proyecto existente: ${projectName}\n`))
140
-
141
+
141
142
  // ── Verificar OpenCode ──
142
- const interactive = options.interactive !== false && !!process.stdin.isTTY
143
143
  await checkAndInstallOpencode(interactive)
144
144
 
145
145
  console.log(chalk.cyan("✨ OPL Init: Entorno detectado."))
146
146
  console.log(chalk.yellow("OPL delega ahora la inicialización estratégica a tu Agente IA."))
147
- console.log(chalk.white("Copia y pega el siguiente prompt en tu chat con la IA (ej. Cline, Cursor, OpenCode, etc.):\n"))
148
-
149
- console.log(chalk.bgGray.white(`
147
+ console.log(
148
+ chalk.white(
149
+ "Copia y pega el siguiente prompt en tu chat con la IA (ej. Cline, Cursor, OpenCode, etc.):\n"
150
+ )
151
+ )
152
+
153
+ console.log(
154
+ chalk.bgGray.white(`
150
155
  "Asume el rol de Arquitecto Principal OPL. Acabo de ejecutar opl init para un proyecto EXISTENTE llamado '${projectName}'.
151
156
 
152
157
  Tu proceso obligatorio es:
@@ -155,7 +160,8 @@ Tu proceso obligatorio es:
155
160
  3. Genera la estructura que falte (docs/, work-context, AGENTS.md, prompt-lang.json, .openprompt/FRAMEWORK.md).
156
161
  4. Organiza un backlog inicial de refactorización con la deuda detectada.
157
162
  5. Confirma explícitamente que el proyecto quedó estabilizado e indícame el siguiente paso operativo."
158
- `))
163
+ `)
164
+ )
159
165
  return cwd
160
166
  }
161
167
 
@@ -426,6 +432,27 @@ Tu proceso obligatorio es:
426
432
  console.log(chalk.yellow(" ⚠️ No se pudo ejecutar upgrade automáticamente."))
427
433
  }
428
434
 
435
+ if (interactive) {
436
+ const { updateKnowledge } = await inquirer.prompt([
437
+ {
438
+ type: "confirm",
439
+ name: "updateKnowledge",
440
+ message:
441
+ "¿Deseas descargar/actualizar la biblioteca de conocimientos (Entorno Académico)?",
442
+ default: false,
443
+ },
444
+ ])
445
+
446
+ if (updateKnowledge) {
447
+ try {
448
+ const { initKnowledgeRepo } = await import("./opl-init-knowledge.js")
449
+ await initKnowledgeRepo(options)
450
+ } catch {
451
+ console.log(chalk.yellow(" ⚠️ No se pudo actualizar el conocimiento automáticamente."))
452
+ }
453
+ }
454
+ }
455
+
429
456
  console.log(chalk.cyan(`\n✅ Integración OLP reconstruida para: ${projectName}`))
430
457
  console.log(chalk.cyan(` Versión OPL: ${config.oplVersion || "desconocida"}`))
431
458
  console.log(chalk.gray(" Archivos generados forzaron actualización."))
@@ -288,7 +288,7 @@ export function setupOpencodeConfig(baseDir, _config) {
288
288
  const opencodeConfig = {
289
289
  $schema: "https://opencode.ai/config.json",
290
290
  mcp: {
291
- openPrompt: { type: "local", command: ["npx", "openprompt-lang", "mcp"], enabled: true },
291
+ openPrompt: { type: "local", command: ["openprompt-lang", "mcp"], enabled: true },
292
292
  },
293
293
  skills: { paths: [".opencode/skills"] },
294
294
  default_agent: "openPrompt",
@@ -535,35 +535,41 @@ export async function selectFullStack(options) {
535
535
  }
536
536
 
537
537
  export async function checkAndInstallOpencode(interactive) {
538
- if (!interactive) return;
538
+ if (!interactive) return
539
539
 
540
- let isInstalled = false;
540
+ let isInstalled = false
541
541
  try {
542
- execSync("opencode --version", { stdio: "ignore" });
543
- isInstalled = true;
542
+ execSync("opencode --version", { stdio: "ignore" })
543
+ isInstalled = true
544
544
  } catch (e) {
545
- isInstalled = false;
545
+ isInstalled = false
546
546
  }
547
547
 
548
548
  if (!isInstalled) {
549
- console.log(chalk.yellow("\n⚠️ No se detectó 'opencode' instalado globalmente."));
550
- console.log(chalk.gray("OpenCode es el agente CLI recomendado para aprovechar la integración MCP de OPL."));
549
+ console.log(chalk.yellow("\n⚠️ No se detectó 'opencode' instalado globalmente."))
550
+ console.log(
551
+ chalk.gray("OpenCode es el agente CLI recomendado para aprovechar la integración MCP de OPL.")
552
+ )
551
553
  const { install } = await inquirer.prompt([
552
554
  {
553
555
  type: "confirm",
554
556
  name: "install",
555
557
  message: "¿Deseas instalar opencode globalmente ahora? (npm install -g opencode)",
556
- default: true
557
- }
558
- ]);
558
+ default: true,
559
+ },
560
+ ])
559
561
 
560
562
  if (install) {
561
- console.log(chalk.cyan("⏳ Instalando opencode..."));
563
+ console.log(chalk.cyan("⏳ Instalando opencode..."))
562
564
  try {
563
- execSync("npm install -g opencode", { stdio: "inherit" });
564
- console.log(chalk.green("✅ opencode instalado exitosamente.\n"));
565
+ execSync("npm install -g opencode", { stdio: "inherit" })
566
+ console.log(chalk.green("✅ opencode instalado exitosamente.\n"))
565
567
  } catch (e) {
566
- console.log(chalk.red("❌ Error instalando opencode. Puedes instalarlo manualmente con: npm install -g opencode\n"));
568
+ console.log(
569
+ chalk.red(
570
+ "❌ Error instalando opencode. Puedes instalarlo manualmente con: npm install -g opencode\n"
571
+ )
572
+ )
567
573
  }
568
574
  }
569
575
  }
@@ -224,7 +224,7 @@ async function setupOpencode(cwd, config, langId, index) {
224
224
  mcp: {
225
225
  openPrompt: {
226
226
  type: "local",
227
- command: ["npx", "openprompt-lang", "mcp"],
227
+ command: ["openprompt-lang", "mcp"],
228
228
  enabled: true,
229
229
  },
230
230
  },
@@ -349,8 +349,8 @@ async function setupVscode(cwd, config, langId) {
349
349
  servers: {
350
350
  "openPrompt-Lang": {
351
351
  type: "stdio",
352
- command: "npx",
353
- args: ["openprompt-lang", "mcp"],
352
+ command: "openprompt-lang",
353
+ args: ["mcp"],
354
354
  },
355
355
  },
356
356
  }
@@ -381,8 +381,8 @@ async function setupAntigravity(cwd, config, langId) {
381
381
  const globalMcp = {
382
382
  mcpServers: {
383
383
  "openPrompt-Lang": {
384
- command: "npx",
385
- args: ["openprompt-lang", "mcp"],
384
+ command: "openprompt-lang",
385
+ args: ["mcp"],
386
386
  },
387
387
  },
388
388
  }
@@ -603,6 +603,15 @@ async function start(description) {
603
603
 
604
604
  writeJSON(SESSION_FILE, session)
605
605
  appendLog("session_start", description)
606
+
607
+ // Sincronizar con MCP shared state (workflow delivery → modo execute)
608
+ try {
609
+ const { syncWorkflowPhase } = await import("../mcp-shared-state.js")
610
+ syncWorkflowPhase("DELIVERY")
611
+ } catch {
612
+ /* silencioso */
613
+ }
614
+
606
615
  console.log(chalk.green(`\n✅ Sesión iniciada: ${session.session.id}\n`))
607
616
  console.log(` ${chalk.bold("Tarea:")} ${description}`)
608
617
  console.log(` ${chalk.bold("Rama:")} ${branch}`)
@@ -824,6 +833,14 @@ async function close() {
824
833
  writeJSON(SESSION_FILE, session)
825
834
  appendLog("session_close", `Sesión cerrada: ${session.session.id}`)
826
835
 
836
+ // Sincronizar con MCP shared state (workflow close → modo plan de nuevo)
837
+ try {
838
+ const { syncWorkflowPhase } = await import("../mcp-shared-state.js")
839
+ syncWorkflowPhase("CLOSE_SESSION")
840
+ } catch {
841
+ /* silencioso */
842
+ }
843
+
827
844
  try {
828
845
  const { updateProjectContext } = await import("./project-context.js")
829
846
  await updateProjectContext(resolveBase())
@@ -16,7 +16,7 @@ import {
16
16
  } from "../../../persistence/sqlite/queries.js"
17
17
  import { executeTransition, canTransition } from "../../../core/workflow/transitions.js"
18
18
  import { generateSessionDocs, generateIndexDoc } from "../../../docgen/session-docs.js"
19
- import { setPhase } from "../../../mcp-shared-state.js"
19
+ import { setPhase, syncWorkflowPhase } from "../../../mcp-shared-state.js"
20
20
  import chalk from "chalk"
21
21
 
22
22
  /**
@@ -148,6 +148,7 @@ export async function closeWorkflow(options = {}) {
148
148
  setMetadata(db, "active_session", "")
149
149
  setMetadata(db, "active_ticket", "")
150
150
  setPhase("idle", process.cwd())
151
+ syncWorkflowPhase("CLOSE_SESSION")
151
152
 
152
153
  // ─── 3. Generar documentación viva ────────────────────────────────────
153
154
  const sessionFiles = generateSessionDocs({
@@ -13,6 +13,7 @@ import {
13
13
  createTicket,
14
14
  createSession,
15
15
  } from "../../../persistence/sqlite/queries.js"
16
+ import { syncWorkflowPhase } from "../../../mcp-shared-state.js"
16
17
  import { executeTransition, canTransition } from "../../../core/workflow/transitions.js"
17
18
  import { getPhase } from "../../../core/workflow/phases.js"
18
19
  import chalk from "chalk"
@@ -133,6 +134,9 @@ export async function delivery(options = {}) {
133
134
  setMetadata(db, "active_ticket", ticketId)
134
135
  setMetadata(db, "workflow_phase", "DELIVERY")
135
136
 
137
+ // Sincronizar con MCP shared state: DELIVERY → modo execute
138
+ syncWorkflowPhase("DELIVERY")
139
+
136
140
  conn.close()
137
141
 
138
142
  const phaseInfo = getPhase("DELIVERY")
@@ -12,6 +12,7 @@ import {
12
12
  canTransition,
13
13
  getInitialContext,
14
14
  } from "../../../core/workflow/transitions.js"
15
+ import { syncWorkflowPhase } from "../../../mcp-shared-state.js"
15
16
  import { runWizard, getWizardResponses } from "../../../wizard/orchestrator.js"
16
17
  import chalk from "chalk"
17
18
 
@@ -75,6 +76,9 @@ export async function discovery(options = {}) {
75
76
  setMetadata(db, "workflow_project_id", projectId)
76
77
  }
77
78
 
79
+ // Sincronizar con MCP shared state: DISCOVERY → modo plan
80
+ syncWorkflowPhase("DISCOVERY")
81
+
78
82
  conn.close()
79
83
 
80
84
  console.log(chalk.green(`\n✅ Discovery completado para: ${projectId}`))
@@ -8,6 +8,7 @@ import { ensureSchema } from "../../../persistence/sqlite/schema.js"
8
8
  import { runMigrations } from "../../../persistence/sqlite/migrations.js"
9
9
  import { getMetadata, setMetadata } from "../../../persistence/sqlite/queries.js"
10
10
  import { executeTransition, canTransition } from "../../../core/workflow/transitions.js"
11
+ import { syncWorkflowPhase } from "../../../mcp-shared-state.js"
11
12
  import { getWizardResponses } from "../../../wizard/orchestrator.js"
12
13
  import { generateDocs } from "../../../docgen/generator.js"
13
14
  import {
@@ -103,6 +104,9 @@ export async function specification(options = {}) {
103
104
  executeTransition(ctx.currentPhase, "SPECIFICATION", ctx)
104
105
  setMetadata(db, "workflow_phase", "SPECIFICATION")
105
106
 
107
+ // Sincronizar con MCP shared state: SPECIFICATION → modo plan
108
+ syncWorkflowPhase("SPECIFICATION")
109
+
106
110
  conn.close()
107
111
 
108
112
  console.log(chalk.green(`\n✅ Specification completada para: ${projectId}`))
@@ -89,10 +89,12 @@ async function executeLocalFast(payload, cwd, startAt, startTime) {
89
89
  const timeout = constraints.timeoutMs || 30000
90
90
  let isTimeout = false
91
91
 
92
+ // shell: false — args ya se dividen manualmente (bin + args).
93
+ // Esto previene command injection si el comando contiene caracteres especiales.
92
94
  const child = spawn(bin, args, {
93
95
  cwd,
94
- shell: true,
95
- env: process.env, // Permitir entorno actual para local-fast
96
+ shell: false,
97
+ env: process.env,
96
98
  })
97
99
 
98
100
  const timeoutId = setTimeout(() => {
@@ -210,8 +212,10 @@ async function executeDockerEphemeral(payload, cwd, startAt, startTime) {
210
212
  let stderrRaw = ""
211
213
  let isTimeout = false
212
214
 
215
+ // shell: false — dockerArgs ya se parsean manualmente como array.
216
+ // Los comandos internos de docker se ejecutan via "sh -c '<cmd>'".
213
217
  const child = spawn("docker", dockerArgs, {
214
- shell: true, // Necesario en Windows, pero útil para que interprete bien las comillas en los argumentos
218
+ shell: false,
215
219
  })
216
220
 
217
221
  const timeoutId = setTimeout(() => {
@@ -57,7 +57,8 @@ const TOOLS = [
57
57
  name: "plan_create",
58
58
  description:
59
59
  "[PLAN MODE] Crear plan formal @workflow a partir de las respuestas del wizard. " +
60
- "Genera un archivo .md en .opencode/work-context/PLANS/ con toda la configuración.",
60
+ "Genera un archivo .md en .opencode/work-context/PLANS/ con toda la configuración. " +
61
+ "NO cambia a EXECUTE automáticamente — usa plan_validate + plan_switch_mode después.",
61
62
  inputSchema: {
62
63
  type: "object",
63
64
  properties: {
@@ -453,9 +454,10 @@ async function handlePlanCreate(args) {
453
454
  })
454
455
 
455
456
  const planContent = loadPlanFile(planId)
456
- const cwd = process.cwd()
457
- approvePlan(planId, cwd)
458
- setMode("execute", cwd)
457
+
458
+ // Nota: NO se cambia a EXECUTE automáticamente.
459
+ // El flujo correcto es: plan_create → plan_validate → plan_switch_mode
460
+ // Esto asegura que el plan esté completo antes de permitir ejecución.
459
461
 
460
462
  return {
461
463
  content: [
@@ -466,7 +468,12 @@ async function handlePlanCreate(args) {
466
468
  type: "plan_created",
467
469
  plan_id: planId,
468
470
  file_path: filePath,
469
- message: `✅ Plan "${project_name}" creado y aprobado. Modo cambiado a EXECUTE.`,
471
+ mode: "plan",
472
+ message: `✅ Plan "${project_name}" creado. Usa plan_validate para verificar y plan_switch_mode para comenzar la ejecución.`,
473
+ next_steps: [
474
+ `plan_validate plan_id="${planId}"`,
475
+ `plan_switch_mode plan_id="${planId}"`,
476
+ ],
470
477
  plan_content: planContent,
471
478
  },
472
479
  null,
@@ -212,4 +212,29 @@ export function isExecutionTool(toolName) {
212
212
  return EXECUTION_TOOLS.includes(toolName)
213
213
  }
214
214
 
215
+ /**
216
+ * Sincroniza el modo MCP según la fase del workflow CLI.
217
+ * Conecta src/commands/workflow/ con el sistema MCP.
218
+ *
219
+ * Fases de planificación (DISCOVERY, SPECIFICATION) → mode "plan"
220
+ * Fases de ejecución (DELIVERY, CLOSE_SESSION) → mode "execute"
221
+ *
222
+ * @param {string} workflowPhase - Fase del workflow (DISCOVERY, SPECIFICATION, DELIVERY, CLOSE_SESSION)
223
+ * @param {string} [cwd] - Directorio del proyecto
224
+ */
225
+ export function syncWorkflowPhase(workflowPhase, cwd) {
226
+ const planPhases = ["DISCOVERY", "SPECIFICATION", "BACKLOG_READY"]
227
+ const executePhases = ["DELIVERY", "CLOSE_SESSION"]
228
+
229
+ if (planPhases.includes(workflowPhase)) {
230
+ const state = getSharedState(cwd)
231
+ // Solo cambiar si no hay un plan aprobado en ejecución
232
+ if (!state.approved_at || state.mode !== "execute") {
233
+ setMode("plan", cwd)
234
+ }
235
+ } else if (executePhases.includes(workflowPhase)) {
236
+ setMode("execute", cwd)
237
+ }
238
+ }
239
+
215
240
  export { PLAN_TOOLS, EXECUTION_TOOLS }
@@ -1,171 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync, existsSync, rmSync, writeFileSync, mkdirSync } from "fs"
4
- import { join, dirname } from "path"
5
- import { fileURLToPath } from "url"
6
- import { TOOLS } from "./tools.js"
7
- import { handleToolCall } from "./router.js"
8
- import { generateRichInstructions } from "../mcp-workflow.js"
9
-
10
- const __filename = fileURLToPath(import.meta.url)
11
- const __dirname = dirname(__filename)
12
-
13
- function getServerVersion() {
14
- try {
15
- const pkg = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"))
16
- return pkg.version || "0.9.0"
17
- } catch {
18
- return "0.9.0"
19
- }
20
- }
21
-
22
- const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05", "1.0", "1.0.0", "0.1.0"]
23
- const LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0]
24
-
25
- const SERVER_INFO = {
26
- name: "OPL",
27
- version: getServerVersion(),
28
- }
29
-
30
- function sendMessage(msg) {
31
- const data = JSON.stringify(msg)
32
- process.stdout.write(`${data}\n`)
33
- }
34
-
35
- function sendError(id, code, message) {
36
- sendMessage({ jsonrpc: "2.0", id, error: { code, message } })
37
- }
38
-
39
- function sendResult(id, result) {
40
- sendMessage({ jsonrpc: "2.0", id, result })
41
- }
42
-
43
- export function startServer() {
44
- // PID lock: evitar servidores duplicados
45
- const PID_PATH = join(process.cwd(), ".opencode", "mcp.pid")
46
- try {
47
- if (existsSync(PID_PATH)) {
48
- const oldPid = readFileSync(PID_PATH, "utf-8").trim()
49
- try {
50
- process.kill(parseInt(oldPid), 0)
51
- console.error("[mcp] ⚠ Ya hay un servidor MCP activo (PID " + oldPid + "). Saliendo.")
52
- process.exit(1)
53
- } catch {
54
- /* PID huérfano, continuar */
55
- }
56
- }
57
- mkdirSync(dirname(PID_PATH), { recursive: true })
58
- writeFileSync(PID_PATH, String(process.pid))
59
- } catch {
60
- /* fallback: continuar sin PID lock */
61
- }
62
-
63
- // Graceful shutdown: limpiar PID file y conexiones
64
- function cleanupAndExit() {
65
- try {
66
- if (existsSync(PID_PATH)) rmSync(PID_PATH)
67
- } catch {
68
- /* ignore */
69
- }
70
- try {
71
- import("../persistence/sqlite/connection.js").then((m) => m.close())
72
- } catch {
73
- /* ignore */
74
- }
75
- process.exit(0)
76
- }
77
- process.on("SIGTERM", cleanupAndExit)
78
- process.on("SIGINT", cleanupAndExit)
79
-
80
- let buffer = ""
81
-
82
- process.stdin.setEncoding("utf-8")
83
- process.stdin.on("data", async (chunk) => {
84
- buffer += chunk
85
- const lines = buffer.split("\n")
86
- buffer = lines.pop() || ""
87
-
88
- for (const line of lines) {
89
- const trimmed = line.trim()
90
- if (!trimmed) continue
91
-
92
- let msg
93
- try {
94
- msg = JSON.parse(trimmed)
95
- } catch {
96
- continue
97
- }
98
-
99
- if (msg.jsonrpc !== "2.0") continue
100
-
101
- const { id, method, params } = msg
102
-
103
- const isNotification = id === undefined || id === null
104
-
105
- switch (method) {
106
- case "initialize": {
107
- const clientVersion = params?.protocolVersion
108
- const clientSupportsLatest =
109
- clientVersion && SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)
110
- const negotiatedVersion = clientSupportsLatest ? clientVersion : LATEST_PROTOCOL_VERSION
111
-
112
- // Generate rich dynamic instructions based on project context
113
- const instructions = generateRichInstructions()
114
-
115
- sendResult(id, {
116
- protocolVersion: negotiatedVersion,
117
- capabilities: {
118
- tools: { listChanged: false },
119
- logging: {},
120
- },
121
- serverInfo: SERVER_INFO,
122
- instructions,
123
- })
124
- break
125
- }
126
-
127
- case "notifications/initialized": {
128
- break
129
- }
130
-
131
- case "tools/list": {
132
- sendResult(id, { tools: TOOLS })
133
- break
134
- }
135
-
136
- case "tools/call": {
137
- try {
138
- const toolName = params?.name
139
- const toolArgs = params?.arguments || {}
140
- const result = await handleToolCall(toolName, toolArgs)
141
- sendResult(id, result)
142
- } catch (err) {
143
- sendError(id, -32603, err.message)
144
- }
145
- break
146
- }
147
-
148
- case "ping": {
149
- sendResult(id, {})
150
- break
151
- }
152
-
153
- default: {
154
- if (!isNotification) {
155
- sendError(id, -32601, `Method not found: ${method}`)
156
- }
157
- break
158
- }
159
- }
160
- }
161
- })
162
-
163
- process.stdin.on("end", () => {
164
- cleanupAndExit()
165
- })
166
- }
167
-
168
- // Start if main module
169
- if (import.meta.url === `file://${process.argv[1]}`) {
170
- startServer()
171
- }