zugzbot 1.0.39 → 1.0.41

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.
@@ -39,7 +39,7 @@ const getPidFilePath = (root: string) => {
39
39
 
40
40
  // Tool: sdd_free_port
41
41
  export const free_port = tool({
42
- description: "Busca y termina de manera forzada cualquier proceso que esté escuchando en un puerto específico",
42
+ description: "Libera un puerto específico de forma forzada.",
43
43
  args: {
44
44
  port: tool.schema.number().describe("El puerto a liberar (ej. 3000 o 8000)")
45
45
  },
@@ -69,7 +69,7 @@ export const free_port = tool({
69
69
 
70
70
  // Tool: sdd_start_server
71
71
  export const start_server = tool({
72
- description: "Inicia un servidor de desarrollo o producción en segundo plano y registra su PID para limpieza posterior",
72
+ description: "Inicia un servidor en segundo plano y registra su PID.",
73
73
  args: {
74
74
  command: tool.schema.string().describe("Comando para iniciar el servidor (ej. 'yarn dev' o 'npm run dev')"),
75
75
  port: tool.schema.number().optional().default(3000).describe("Puerto esperado del servidor (default: 3000)"),
@@ -132,7 +132,7 @@ export const start_server = tool({
132
132
 
133
133
  // Tool: sdd_stop_server
134
134
  export const stop_server = tool({
135
- description: "Detiene el servidor en segundo plano usando el PID registrado",
135
+ description: "Detiene el servidor en segundo plano registrado.",
136
136
  args: {},
137
137
  async execute(args, context) {
138
138
  const root = getRoot(context)
@@ -10,7 +10,7 @@ const getRoot = (context: any) => {
10
10
  };
11
11
 
12
12
  export default tool({
13
- description: "Obtiene de forma nativa y unificada el estado del ciclo SDD (.openspec/sdd_state.json) junto con un resumen de las categorías de memoria del cerebro (Brain). Evita llamadas lentas de consola y es multiplataforma.",
13
+ description: "Obtiene el estado unificado del ciclo SDD y un resumen de las categorías del Brain.",
14
14
  args: {},
15
15
  async execute(args, context) {
16
16
  const root = getRoot(context)
@@ -71,7 +71,7 @@ const parseSemanticErrors = (rawOutput: string, type: "eslint" | "tsc"): any[] =
71
71
 
72
72
  // Tool: sdd_quick_lint
73
73
  export const quick_lint = tool({
74
- description: "Ejecuta el linter del proyecto (eslint) restringido a src/. Devuelve exit code y warnings. Usado como gate automático antes de transicionar a F3.",
74
+ description: "Ejecuta el linter del proyecto (eslint) restringido a src/.",
75
75
  args: {},
76
76
  async execute(args, context) {
77
77
  const root = getRoot(context)
@@ -140,7 +140,7 @@ export const quick_lint = tool({
140
140
 
141
141
  // Tool: sdd_shift_left_verify
142
142
  export const shift_left_verify = tool({
143
- description: "Ejecuta validaciones estáticas Shift-Left completas en el proyecto: ejecuta el compilador de TypeScript (tsc) y el linter (eslint) de manera combinada. Limpia y parsea semánticamente los stack traces y logs de error crudos, devolviendo un JSON limpio y estructurado de errores que el LLM puede digerir y solucionar de inmediato sin perder atención.",
143
+ description: "Ejecuta validaciones estáticas Shift-Left completas (tsc + eslint) combinadas.",
144
144
  args: {},
145
145
  async execute(args, context) {
146
146
  const root = getRoot(context)
@@ -323,7 +323,7 @@ export const shift_left_verify = tool({
323
323
 
324
324
  // Tool: sdd_generate_tests
325
325
  export const generate_tests = tool({
326
- description: "Autogenera plantillas de pruebas unitarias/integración en el directorio corporativo de pruebas a partir de los escenarios de prueba descritos en el contrato activo de sdd_state.json. No pisa archivos de pruebas existentes.",
326
+ description: "Autogenera plantillas de pruebas a partir de los escenarios del contrato activo.",
327
327
  args: {},
328
328
  async execute(args, context) {
329
329
  const root = getRoot(context)
@@ -442,7 +442,7 @@ export const generate_tests = tool({
442
442
 
443
443
  // Tool: sdd_save_playwright_artifacts
444
444
  export const save_playwright_artifacts = tool({
445
- description: "Promueve y archiva los artefactos de captura visual/videos (.openspec/.playwright/) generados por Playwright a la carpeta del spec/contrato activo de forma ordenada, evitando ensuciar la raíz.",
445
+ description: "Promueve y archiva los artefactos de Playwright a la carpeta del contrato activo.",
446
446
  args: {
447
447
  callId: tool.schema.string().optional().describe("ID de llamada de Playwright MCP específica. Si no se pasa, toma la última ejecución de forma automática."),
448
448
  move: tool.schema.boolean().default(false).describe("Si true, mueve los archivos en lugar de copiarlos.")
@@ -9,7 +9,7 @@ DB_PATH = "/Users/wavesbyte/.local/share/opencode/opencode.db"
9
9
 
10
10
  # Escribe aquí el ID de la sesión que deseas exportar (ejemplo: "ses_1234...")
11
11
  # Si se deja vacío, el script requerirá el ID como argumento al ejecutarlo
12
- TARGET_SESSION_ID = "ses_11ca"
12
+ TARGET_SESSION_ID = "ses_11a7"
13
13
 
14
14
 
15
15
  def format_timestamp(ts):
@@ -4,54 +4,11 @@ import os
4
4
  import json
5
5
  import re
6
6
  import curses
7
+ import subprocess
7
8
 
8
9
  # ==============================================================================
9
- # LISTA DE MODELOS DISPONIBLES EN OPENCODE
10
+ # CONFIGURACIÓN Y RUTAS
10
11
  # ==============================================================================
11
- AVAILABLE_MODELS = [
12
- "opencode/big-pickle",
13
- "opencode/deepseek-v4-flash-free",
14
- "opencode/mimo-v2.5-free",
15
- "opencode/nemotron-3-ultra-free",
16
- "opencode/north-mini-code-free",
17
- "deepseek/deepseek-chat",
18
- "deepseek/deepseek-reasoner",
19
- "deepseek/deepseek-v4-flash",
20
- "deepseek/deepseek-v4-pro",
21
- "google/gemini-2.5-flash",
22
- "google/gemini-2.5-flash-image",
23
- "google/gemini-2.5-flash-lite",
24
- "google/gemini-2.5-flash-preview-tts",
25
- "google/gemini-2.5-pro",
26
- "google/gemini-2.5-pro-preview-tts",
27
- "google/gemini-3-flash-preview",
28
- "google/gemini-3-pro-image-preview",
29
- "google/gemini-3.1-flash-image-preview",
30
- "google/gemini-3.1-flash-lite",
31
- "google/gemini-3.1-pro-preview",
32
- "google/gemini-3.1-pro-preview-customtools",
33
- "google/gemini-3.5-flash",
34
- "google/gemini-embedding-001",
35
- "google/gemini-flash-latest",
36
- "google/gemini-flash-lite-latest",
37
- "google/gemma-4-26b-a4b-it",
38
- "google/gemma-4-31b-it",
39
- "minimax/MiniMax-M2",
40
- "minimax/MiniMax-M2.1",
41
- "minimax/MiniMax-M2.5",
42
- "minimax/MiniMax-M2.5-highspeed",
43
- "minimax/MiniMax-M2.7",
44
- "minimax/MiniMax-M2.7-highspeed",
45
- "minimax/MiniMax-M3",
46
- "minimax-coding-plan/MiniMax-M2",
47
- "minimax-coding-plan/MiniMax-M2.1",
48
- "minimax-coding-plan/MiniMax-M2.5",
49
- "minimax-coding-plan/MiniMax-M2.5-highspeed",
50
- "minimax-coding-plan/MiniMax-M2.7",
51
- "minimax-coding-plan/MiniMax-M2.7-highspeed",
52
- "minimax-coding-plan/MiniMax-M3",
53
- ]
54
-
55
12
  AGENTS_LIST = ["sdd-orchestrator", "sdd-spec-writer", "sdd-coder", "sdd-tester", "sdd-deployer"]
56
13
 
57
14
 
@@ -64,10 +21,44 @@ def get_paths():
64
21
  return opencode_json, agents_dir, models_json
65
22
 
66
23
 
24
+ def fetch_available_models(silent=False):
25
+ """Obtiene los modelos disponibles ejecutando el comando de opencode de forma dinámica."""
26
+ if not silent:
27
+ print("🔍 Cargando modelos desde opencode...", end="", flush=True)
28
+
29
+ try:
30
+ # Ejecutar 'opencode models' con un timeout de 4 segundos
31
+ res = subprocess.run(["opencode", "models"], capture_output=True, text=True, timeout=4)
32
+ if res.returncode == 0 and res.stdout.strip():
33
+ models = [line.strip() for line in res.stdout.strip().split("\n") if line.strip()]
34
+ if not silent:
35
+ print(" ¡Listo! 🎉")
36
+ return models
37
+ except Exception:
38
+ pass
39
+
40
+ if not silent:
41
+ print(" (Usando lista de respaldo de emergencia) ⚠️")
42
+
43
+ # Lista de respaldo en caso de que opencode models falle
44
+ return [
45
+ "google/gemini-3.5-flash",
46
+ "google/gemini-3.1-pro-preview",
47
+ "google/gemini-2.5-pro",
48
+ "google/gemini-2.5-flash",
49
+ "deepseek/deepseek-v4-flash",
50
+ "deepseek/deepseek-reasoner",
51
+ "opencode/deepseek-v4-flash-free",
52
+ "opencode/nemotron-3-ultra-free",
53
+ "opencode/mimo-v2.5-free",
54
+ "opencode/big-pickle",
55
+ ]
56
+
57
+
67
58
  def load_models_config():
68
59
  opencode_json, agents_dir, models_json = get_paths()
69
60
  default_config = {
70
- "global": "deepseek/deepseek-v4-flash",
61
+ "global": "google/gemini-3.5-flash",
71
62
  "sdd-orchestrator": "",
72
63
  "sdd-spec-writer": "",
73
64
  "sdd-coder": "",
@@ -82,7 +73,6 @@ def load_models_config():
82
73
  except Exception:
83
74
  pass
84
75
 
85
- # Guardamos el default si no existe
86
76
  try:
87
77
  with open(models_json, 'w', encoding='utf-8') as f:
88
78
  json.dump(default_config, f, indent=2)
@@ -92,7 +82,7 @@ def load_models_config():
92
82
 
93
83
 
94
84
  def save_models_config(config):
95
- opencode_json, agents_dir, models_json = get_paths()
85
+ _, _, models_json = get_paths()
96
86
  try:
97
87
  with open(models_json, 'w', encoding='utf-8') as f:
98
88
  json.dump(config, f, indent=2)
@@ -100,14 +90,8 @@ def save_models_config(config):
100
90
  print(f"Error guardando models.json: {e}", file=sys.stderr)
101
91
 
102
92
 
103
- def search_model(query):
104
- query = query.lower()
105
- matches = [m for m in AVAILABLE_MODELS if query in m.lower()]
106
- return matches
107
-
108
-
109
93
  def apply_model(agent_models):
110
- opencode_json, agents_dir, models_json = get_paths()
94
+ opencode_json, agents_dir, _ = get_paths()
111
95
 
112
96
  if not os.path.exists(opencode_json):
113
97
  print(f"Error: No se encontró {opencode_json}", file=sys.stderr)
@@ -130,7 +114,6 @@ def apply_model(agent_models):
130
114
  if os.path.exists(agents_dir):
131
115
  for filename in os.listdir(agents_dir):
132
116
  if filename.endswith(".md"):
133
- # Quitar extensión .md para comparar con el nombre del agente
134
117
  agent_name = filename[:-3]
135
118
  if agent_name in agent_models:
136
119
  target_model = agent_models[agent_name]
@@ -162,26 +145,14 @@ def apply_model(agent_models):
162
145
  print(f"Advertencia: Directorio de agentes {agents_dir} no existe.")
163
146
 
164
147
 
165
- def print_available_models():
166
- print("\nModelos disponibles:")
167
- for m in AVAILABLE_MODELS:
168
- print(f" - {m}")
169
- print()
170
-
171
-
172
148
  # ==============================================================================
173
149
  # INTERFAZ INTERACTIVA TUI (CURSES)
174
150
  # ==============================================================================
175
151
 
176
- def select_model_tui(stdscr, title, current_val, is_global=False):
177
- # Habilitar modo keypad para recibir flechas de cursor
152
+ def select_model_tui(stdscr, title, current_val, models_list, is_global=False):
153
+ """Pantalla interactiva de búsqueda con filtro en tiempo real para seleccionar un modelo."""
178
154
  stdscr.keypad(True)
179
- curses.curs_set(0) # Ocultar cursor físico
180
-
181
- # Colores
182
- curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Seleccionado
183
- curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) # Buscador
184
- curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) # Éxito / Info
155
+ curses.curs_set(1) # Mostrar cursor para el buscador
185
156
 
186
157
  query = ""
187
158
  selected_idx = 0
@@ -189,70 +160,72 @@ def select_model_tui(stdscr, title, current_val, is_global=False):
189
160
  while True:
190
161
  stdscr.clear()
191
162
 
192
- # Obtener lista de modelos y añadir "Heredar del Global" si no es la config global
163
+ # 1. Preparar lista de opciones filtrada
193
164
  options = []
194
165
  if not is_global:
195
- options.append("") # Representa [Heredar del Global]
166
+ options.append("") # Representa [Heredar del Global]
196
167
 
197
- # Filtrar modelos según query
198
- filtered_models = [m for m in AVAILABLE_MODELS if query.lower() in m.lower()]
168
+ filtered_models = [m for m in models_list if query.lower() in m.lower()]
199
169
  options.extend(filtered_models)
200
170
 
201
- # Validar límites de selección
171
+ # Ajustar límites de selección
202
172
  if selected_idx >= len(options):
203
173
  selected_idx = max(0, len(options) - 1)
204
174
 
205
- # Títulos
206
- stdscr.addstr(1, 2, f"=== {title} ===", curses.A_BOLD)
207
- stdscr.addstr(2, 2, "Escribe caracteres para buscar en tiempo real.", curses.A_DIM)
208
- stdscr.addstr(3, 2, "Usa [↑/↓] para navegar y [Enter] para confirmar. [ESC] para volver.", curses.A_DIM)
175
+ # 2. Renderizar interfaz (Estilo minimalista y sobrio)
176
+ stdscr.addstr(1, 2, f"┌─── {title} ───┐", curses.A_BOLD)
177
+ stdscr.addstr(2, 2, "Escribe para buscar en tiempo real, [↑/↓] navega, [Enter] confirma.", curses.A_DIM)
178
+ stdscr.addstr(3, 2, " Presiona [ESC] para cancelar y volver atrás sin guardar.", curses.A_DIM)
179
+
180
+ # Input de búsqueda
181
+ stdscr.addstr(5, 2, "🔍 Buscar: ")
182
+ stdscr.addstr(5, 13, query, curses.A_BOLD)
209
183
 
210
- # Caja de búsqueda
211
- stdscr.addstr(5, 2, "Buscador: ", curses.A_BOLD)
212
- stdscr.addstr(5, 12, f" {query}█ ", curses.color_pair(2) | curses.A_BOLD)
213
- stdscr.addstr(6, 2, "-" * 60, curses.A_DIM)
184
+ # Línea divisoria discreta
185
+ stdscr.addstr(6, 2, "─" * 70, curses.A_DIM)
214
186
 
215
- # Mostrar opciones con paginación simple de terminal
187
+ # Mostrar opciones paginadas
216
188
  start_row = 8
217
189
  max_rows = curses.LINES - start_row - 2
218
190
 
219
191
  if not options:
220
- stdscr.addstr(start_row, 4, "No hay modelos que coincidan con la búsqueda.", curses.color_pair(2))
192
+ stdscr.addstr(start_row, 4, "No hay modelos que coincidan con la búsqueda.", curses.A_BOLD)
221
193
  else:
222
- # Mostrar solo lo que quepa en la pantalla
223
194
  for i in range(min(len(options), max_rows)):
224
195
  opt = options[i]
225
196
  row = start_row + i
226
197
 
227
- # Formatear el label
198
+ # Formatear el texto a mostrar
228
199
  if opt == "":
229
- label = "[ Heredar del Global ]"
200
+ label = " [ Heredar del Modelo Global ]"
230
201
  else:
231
- label = opt
202
+ label = f" {opt}"
232
203
 
233
- # Indicar si es el activo actual
204
+ # Añadir tag si es el actualmente activo
234
205
  if opt == current_val:
235
206
  label += " (activo)"
236
207
 
237
208
  # Resaltar la opción seleccionada
238
209
  if i == selected_idx:
239
- stdscr.attron(curses.color_pair(1))
240
- stdscr.addstr(row, 2, f" > {label:<50} ")
241
- stdscr.attroff(curses.color_pair(1))
210
+ stdscr.attron(curses.A_REVERSE)
211
+ stdscr.addstr(row, 2, f" {label:<66} ")
212
+ stdscr.attroff(curses.A_REVERSE)
242
213
  else:
243
- stdscr.addstr(row, 2, f" {label:<50}")
214
+ stdscr.addstr(row, 2, f" {label:<66}")
244
215
 
216
+ # Mover el cursor al final de la búsqueda para feedback visual
217
+ stdscr.move(5, 13 + len(query))
245
218
  stdscr.refresh()
246
219
 
247
- # Leer tecla
220
+ # 3. Procesar teclado
248
221
  try:
249
222
  key = stdscr.getch()
250
223
  except KeyboardInterrupt:
251
224
  return None
252
225
 
253
- if key in (27, curses.KEY_CANCEL): # ESC
226
+ if key in (27, curses.KEY_CANCEL): # ESC
254
227
  return None
255
- elif key in (10, 13, curses.KEY_ENTER): # Enter
228
+ elif key in (10, 13, curses.KEY_ENTER): # Enter
256
229
  if options:
257
230
  return options[selected_idx]
258
231
  return None
@@ -262,24 +235,19 @@ def select_model_tui(stdscr, title, current_val, is_global=False):
262
235
  elif key == curses.KEY_DOWN:
263
236
  if selected_idx < len(options) - 1:
264
237
  selected_idx += 1
265
- elif key in (127, 8, curses.KEY_BACKSPACE): # Backspace
238
+ elif key in (127, 8, curses.KEY_BACKSPACE): # Backspace
266
239
  query = query[:-1]
267
240
  selected_idx = 0
268
- elif 32 <= key <= 126: # Carácter imprimible
241
+ elif 32 <= key <= 126: # Caracteres imprimibles
269
242
  query += chr(key)
270
243
  selected_idx = 0
271
244
 
272
245
 
273
- def main_tui(stdscr):
274
- # Habilitar keypad
246
+ def main_tui(stdscr, models_list):
247
+ """Menú de configuración principal."""
275
248
  stdscr.keypad(True)
276
- curses.curs_set(0)
277
-
278
- # Inicializar pares de colores
279
- curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Selección
280
- curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) # Alertas
281
- curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) # Éxito
282
- curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Warnings
249
+ curses.curs_set(0) # Ocultar cursor físico
250
+ curses.use_default_colors() # Respetar fondo de la terminal del usuario
283
251
 
284
252
  config = load_models_config()
285
253
  selected_row = 0
@@ -287,57 +255,71 @@ def main_tui(stdscr):
287
255
  while True:
288
256
  stdscr.clear()
289
257
 
290
- stdscr.addstr(1, 2, "🤖 ZUGZBOT: CONFIGURACIÓN INTERACTIVA DE MODELOS", curses.A_BOLD)
291
- stdscr.addstr(2, 2, "Navega con [↑/↓], pulsa [Enter] para editar un agente.", curses.A_DIM)
292
- stdscr.addstr(3, 2, "Presiona [G] para Guardar y Aplicar, o [Q]/[ESC] para Salir.", curses.A_DIM)
293
- stdscr.addstr(4, 2, "" * 70)
258
+ # Cabecera moderna y minimalista
259
+ stdscr.addstr(1, 2, "🤖 ZUGZBOT: CONFIGURACIÓN DE MODELOS", curses.A_BOLD)
260
+ stdscr.addstr(2, 2, "Navega con [↑/↓], pulsa [Enter] para cambiar el modelo de un agente.", curses.A_DIM)
261
+ stdscr.addstr(3, 2, "Presiona [G] para guardar y aplicar, o [ESC]/[Q] para salir sin cambios.", curses.A_DIM)
262
+ stdscr.addstr(4, 2, "═" * 74, curses.A_BOLD)
294
263
 
295
- # Construir items del menú interactivo
296
- global_val = config.get("global", "deepseek/deepseek-v4-flash")
264
+ # Construir items de la tabla
265
+ global_val = config.get("global", "google/gemini-3.5-flash")
297
266
 
298
267
  menu_items = [
299
- ("global", f"Modelo Global: {global_val}"),
268
+ ("global", "Modelo Global (Defecto)", global_val),
300
269
  ]
301
270
 
302
271
  for agent in AGENTS_LIST:
303
272
  val = config.get(agent, "")
304
- label = val if val else f"[Hereda del global: {global_val}]"
305
- menu_items.append((agent, f"{agent}: {label}"))
273
+ label = val if val else f"Heredado -> {global_val}"
274
+ menu_items.append((agent, agent, label))
306
275
 
307
276
  # Añadir opciones de acción al menú
308
- menu_items.append(("save", "[✓] GUARDAR Y APLICAR CAMBIOS"))
309
- menu_items.append(("cancel", "[✗] CANCELAR Y SALIR"))
277
+ menu_items.append(("save", " [✓] GUARDAR Y APLICAR CAMBIOS", ""))
278
+ menu_items.append(("cancel", " [✗] CANCELAR Y SALIR", ""))
310
279
 
280
+ # Dibujar cabecera de la tabla
311
281
  start_row = 6
312
- for idx, (key, text) in enumerate(menu_items):
313
- row = start_row + idx
282
+ stdscr.addstr(start_row, 4, f"{'AGENTE / CONTEXTO':<28} │ {'MODELO ASIGNADO':<35}", curses.A_BOLD)
283
+ stdscr.addstr(start_row + 1, 2, "─" * 74, curses.A_DIM)
284
+
285
+ # Renderizar cada fila
286
+ item_start_row = start_row + 2
287
+ for idx, item in enumerate(menu_items):
288
+ key, col1, col2 = item
289
+ row = item_start_row + idx
314
290
 
315
- # Resaltar la opción actual
291
+ # Formatear fila
292
+ if key in ("save", "cancel"):
293
+ text = f" {col1:<70} "
294
+ else:
295
+ text = f" {col1:<26} │ {col2:<38} "
296
+
297
+ # Resaltar la fila seleccionada
316
298
  if idx == selected_row:
317
- stdscr.attron(curses.color_pair(1))
318
- stdscr.addstr(row, 2, f" > {text:<65} ")
319
- stdscr.attroff(curses.color_pair(1))
299
+ stdscr.attron(curses.A_REVERSE)
300
+ stdscr.addstr(row, 2, f" {text:<70} ")
301
+ stdscr.attroff(curses.A_REVERSE)
320
302
  else:
321
303
  if key == "save":
322
- stdscr.addstr(row, 2, f" {text}", curses.color_pair(3) | curses.A_BOLD)
304
+ stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
323
305
  elif key == "cancel":
324
- stdscr.addstr(row, 2, f" {text}", curses.color_pair(2))
306
+ stdscr.addstr(row, 2, f" {text:<70} ", curses.A_DIM)
325
307
  elif key == "global":
326
- stdscr.addstr(row, 2, f" {text}", curses.color_pair(3))
308
+ stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
327
309
  else:
328
- stdscr.addstr(row, 2, f" {text}")
310
+ stdscr.addstr(row, 2, f" {text:<70} ")
329
311
 
330
312
  stdscr.refresh()
331
313
 
332
- # Leer acción del usuario
314
+ # Leer teclas
333
315
  try:
334
316
  key = stdscr.getch()
335
317
  except KeyboardInterrupt:
336
318
  break
337
319
 
338
- if key in (ord('q'), ord('Q'), 27): # Q o ESC
320
+ if key in (ord('q'), ord('Q'), 27): # Q o ESC
339
321
  break
340
- elif key in (ord('g'), ord('G')): # G de Guardar
322
+ elif key in (ord('g'), ord('G')): # G para Guardar directo
341
323
  apply_and_exit(config)
342
324
  break
343
325
  elif key == curses.KEY_UP:
@@ -347,8 +329,7 @@ def main_tui(stdscr):
347
329
  if selected_row < len(menu_items) - 1:
348
330
  selected_row += 1
349
331
  elif key in (10, 13, curses.KEY_ENTER):
350
- # Obtener el item seleccionado
351
- action_key, _ = menu_items[selected_row]
332
+ action_key = menu_items[selected_row][0]
352
333
 
353
334
  if action_key == "save":
354
335
  apply_and_exit(config)
@@ -356,39 +337,39 @@ def main_tui(stdscr):
356
337
  elif action_key == "cancel":
357
338
  break
358
339
  elif action_key == "global":
359
- # Editar el modelo global
360
- new_val = select_model_tui(stdscr, "SELECCIONAR MODELO GLOBAL POR DEFECTO", global_val, is_global=True)
340
+ # Cambiar modelo global
341
+ new_val = select_model_tui(stdscr, "SELECCIONAR MODELO GLOBAL POR DEFECTO", global_val, models_list, is_global=True)
361
342
  if new_val is not None:
362
343
  config["global"] = new_val
363
344
  save_models_config(config)
364
345
  else:
365
- # Editar un agente específico
346
+ # Cambiar modelo de un agente específico
366
347
  current_agent_val = config.get(action_key, "")
367
- new_val = select_model_tui(stdscr, f"SELECCIONAR MODELO PARA {action_key}", current_agent_val, is_global=False)
348
+ new_val = select_model_tui(stdscr, f"SELECCIONAR MODELO PARA {action_key.upper()}", current_agent_val, models_list, is_global=False)
368
349
  if new_val is not None:
369
350
  config[action_key] = new_val
370
351
  save_models_config(config)
371
352
 
372
353
 
373
354
  def apply_and_exit(config):
374
- # Guardar la configuración
375
355
  save_models_config(config)
376
356
 
377
- # Aplicar el mapeo de modelos final en cascada
378
- global_model = config.get("global", "deepseek/deepseek-v4-flash")
357
+ global_model = config.get("global", "google/gemini-3.5-flash")
379
358
  final_agent_models = {}
380
359
 
381
- print("\nMapeo de modelos resultante:")
360
+ print("\n" + "=" * 60)
361
+ print("📋 MAPEO DE MODELOS APLICADO:")
362
+ print("=" * 60)
382
363
  for agent_name in AGENTS_LIST:
383
364
  custom_val = config.get(agent_name, "")
384
365
  final_model = custom_val if custom_val else global_model
385
366
  final_agent_models[agent_name] = final_model
386
- origin = "personalizado" if custom_val else "global"
387
- print(f" * {agent_name} -> {final_model} ({origin})")
367
+ origin = "Personalizado" if custom_val else "Heredado del Global"
368
+ print(f" * {agent_name:<18} -> {final_model:<30} ({origin})")
388
369
 
389
- print("\nAplicando cambios...")
370
+ print("\nAplicando cambios en los archivos de configuración...")
390
371
  apply_model(final_agent_models)
391
- print("\nModelos configurados exitosamente! 🎉")
372
+ print("\n🎉 Configuración completada con éxito.")
392
373
 
393
374
 
394
375
  # ==============================================================================
@@ -397,44 +378,40 @@ def apply_and_exit(config):
397
378
 
398
379
  def main():
399
380
  args = sys.argv[1:]
400
-
401
- # Cargar la configuración actual del JSON
402
381
  config = load_models_config()
403
382
 
404
- # Si se pide listar modelos por CLI
383
+ # Si es modo CLI rápido sin argumentos, o se pide listar
405
384
  if args and args[0] in ("--list", "-l", "list"):
406
- print_available_models()
385
+ models = fetch_available_models(silent=True)
386
+ print("\nModelos disponibles en opencode:")
387
+ for m in models:
388
+ print(f" - {m}")
407
389
  sys.exit(0)
408
390
 
409
- # Si se pasan argumentos por parámetro, corre en modo CLI no-interactivo rápido
391
+ # Si se pasan argumentos, procesa actualización por CLI no-interactiva rápida
410
392
  if args:
411
393
  query = args[0]
412
- matches = search_model(query)
394
+ models = fetch_available_models(silent=True)
395
+ matches = [m for m in models if query.lower() in m.lower()]
413
396
 
414
397
  if not matches:
415
- print(
416
- f"Error: Ningún modelo coincide con '{query}'. Use 'list' para ver todos.",
417
- file=sys.stderr,
418
- )
398
+ print(f"Error: Ningún modelo coincide con '{query}'.", file=sys.stderr)
419
399
  sys.exit(1)
420
400
  elif len(matches) == 1:
421
- global_model_selected = matches[0]
401
+ selected_model = matches[0]
422
402
  else:
423
- # Si hay múltiples coincidencias, tomamos la primera pero listamos las demás
424
- global_model_selected = matches[0]
425
- print(f"Coincidencias encontradas para '{query}':")
403
+ selected_model = matches[0]
404
+ print(f"Múltiples coincidencias para '{query}':")
426
405
  for m in matches:
427
406
  print(f" * {m}")
428
- print(
429
- f"Seleccionando automáticamente la primera: {global_model_selected}\n"
430
- )
407
+ print(f"Seleccionando la primera por defecto: {selected_model}\n")
431
408
 
432
- # Actualizar el global_model en config y persistir
433
- config["global"] = global_model_selected
409
+ config["global"] = selected_model
434
410
  apply_and_exit(config)
435
411
  else:
436
- # Si NO se pasan argumentos, inicia la TUI interactiva
437
- curses.wrapper(main_tui)
412
+ # Modo interactivo elegante con curses
413
+ models_list = fetch_available_models(silent=False)
414
+ curses.wrapper(main_tui, models_list)
438
415
 
439
416
 
440
417
  if __name__ == "__main__":
package/opencode.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "permission": {
7
7
  "webfetch": "deny"
8
8
  },
9
- "model": "deepseek/deepseek-v4-flash"
9
+ "model": "google/gemini-3.5-flash"
10
10
  }
11
11
  },
12
12
  "permission": {
@@ -43,7 +43,7 @@
43
43
  "./.opencode/oh-my-design/packages/mcp/dist/index.mjs"
44
44
  ],
45
45
  "enabled": true,
46
- "purpose": "MCP oficial de Oh My Design para buscar, consultar y aplicar temas de diseño (Toss, Stripe, Supabase, etc.)."
46
+ "purpose": "Busca, consulta y aplica temas de dise\u00f1o (Toss, Stripe, Supabase, etc.)."
47
47
  },
48
48
  "shadcn": {
49
49
  "type": "local",
@@ -54,7 +54,7 @@
54
54
  "mcp"
55
55
  ],
56
56
  "enabled": true,
57
- "purpose": "Librería UI objetivo. Usar para browse/search/install de componentes y blocks (dashboards, landings, auth, forms)."
57
+ "purpose": "Usa para browse/search/install de componentes y blocks oficiales de Shadcn UI."
58
58
  },
59
59
  "context7": {
60
60
  "type": "local",
@@ -76,7 +76,7 @@
76
76
  "--isolated"
77
77
  ],
78
78
  "enabled": true,
79
- "purpose": "Playwright MCP. Artefactos (screenshots, traces, videos, downloads) van a .openspec/.playwright/<call-id>/. Para evidencia por sesión SDD, promueve los artefactos relevantes a .openspec/specs/<activeContract>/playwright/ con .opencode/tools/save-playwright-artifacts.sh. --isolated evita ensuciar el perfil del browser en disco."
79
+ "purpose": "Ejecuta pruebas de regresi\u00f3n visual y capturas de pantalla de Playwright."
80
80
  },
81
81
  "next-devtools": {
82
82
  "type": "local",
@@ -89,7 +89,7 @@
89
89
  "environment": {
90
90
  "NEXT_TELEMETRY_DISABLED": "1"
91
91
  },
92
- "purpose": "Toolchain oficial Vercel para Next.js 16. Provee init, nextjs_docs, nextjs_index/nextjs_call (errores, logs, rutas, Server Actions en runtime), browser_eval, upgrade_nextjs_16 y enable_cache_components. Complementa context7 (docs genérico) con docs y runtime específicos de Next 16 (dev server expone /_next/mcp). Telemetría desactivada explícitamente. **DESHABILITADO por defecto** — solo activar on-demand cuando un subagente reporte un error específico de Next 16 que requiera docs oficiales. La razón: el tool `init` inyecta 1000+ palabras de 'FORGET ALL PRIOR KNOWLEDGE' en el contexto del coder (~8K tokens de reasoning desperdiciado en sesiones normales)."
92
+ "purpose": "Toolchain oficial para depurar Next.js 16 (deshabilitado por defecto)."
93
93
  },
94
94
  "lucide-icons": {
95
95
  "type": "local",
@@ -100,7 +100,7 @@
100
100
  "--stdio"
101
101
  ],
102
102
  "enabled": true,
103
- "purpose": "Permite buscar, filtrar y obtener ejemplos de uso JSX de más de 1,500 iconos de Lucide React de forma exacta mediante herramientas del MCP."
103
+ "purpose": "Busca y obtiene ejemplos de uso de iconos de Lucide React."
104
104
  }
105
105
  }
106
106
  }