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.
- package/.opencode/agents/sdd-coder.md +1 -1
- package/.opencode/agents/sdd-deployer.md +1 -1
- package/.opencode/agents/sdd-orchestrator.md +1 -1
- package/.opencode/agents/sdd-reviewer.md +1 -1
- package/.opencode/agents/sdd-spec-writer.md +1 -1
- package/.opencode/agents/sdd-tester.md +1 -1
- package/.opencode/rules/sdd-global.md +10 -3
- package/.opencode/skills/docker-templates/SKILL.md +7 -0
- package/.opencode/skills/sdd-methodology/SKILL.md +29 -65
- package/.opencode/skills/sdd-quickstart/SKILL.md +1 -0
- package/.opencode/skills/sdd-tester-quickstart/SKILL.md +41 -11
- package/.opencode/skills/shadcn-templates/SKILL.md +32 -18
- package/.opencode/tools/brain.ts +2 -2
- package/.opencode/tools/sdd_bootstrap.ts +4 -4
- package/.opencode/tools/sdd_core.ts +6 -6
- package/.opencode/tools/sdd_design.ts +4 -4
- package/.opencode/tools/sdd_docker.ts +2 -2
- package/.opencode/tools/sdd_network.ts +3 -3
- package/.opencode/tools/sdd_status.ts +1 -1
- package/.opencode/tools/sdd_testing.ts +4 -4
- package/.utils/export_opencode_session.py +1 -1
- package/.utils/toggle_model.py +145 -168
- package/opencode.json +6 -6
- package/package.json +1 -1
|
@@ -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: "
|
|
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
|
|
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
|
|
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
|
|
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/.
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
12
|
+
TARGET_SESSION_ID = "ses_11a7"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def format_timestamp(ts):
|
package/.utils/toggle_model.py
CHANGED
|
@@ -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
|
-
#
|
|
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": "
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
163
|
+
# 1. Preparar lista de opciones filtrada
|
|
193
164
|
options = []
|
|
194
165
|
if not is_global:
|
|
195
|
-
options.append("")
|
|
166
|
+
options.append("") # Representa [Heredar del Global]
|
|
196
167
|
|
|
197
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
206
|
-
stdscr.addstr(1, 2, f"
|
|
207
|
-
stdscr.addstr(2, 2, "Escribe
|
|
208
|
-
stdscr.addstr(3, 2, "
|
|
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
|
-
#
|
|
211
|
-
stdscr.addstr(
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
240
|
-
stdscr.addstr(row, 2, f"
|
|
241
|
-
stdscr.attroff(curses.
|
|
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"
|
|
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
|
-
#
|
|
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):
|
|
226
|
+
if key in (27, curses.KEY_CANCEL): # ESC
|
|
254
227
|
return None
|
|
255
|
-
elif key in (10, 13, curses.KEY_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):
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
stdscr.addstr(
|
|
292
|
-
stdscr.addstr(
|
|
293
|
-
stdscr.addstr(
|
|
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
|
|
296
|
-
global_val = config.get("global", "
|
|
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",
|
|
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"
|
|
305
|
-
menu_items.append((agent,
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
#
|
|
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.
|
|
318
|
-
stdscr.addstr(row, 2, f"
|
|
319
|
-
stdscr.attroff(curses.
|
|
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"
|
|
304
|
+
stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
|
|
323
305
|
elif key == "cancel":
|
|
324
|
-
stdscr.addstr(row, 2, f"
|
|
306
|
+
stdscr.addstr(row, 2, f" {text:<70} ", curses.A_DIM)
|
|
325
307
|
elif key == "global":
|
|
326
|
-
stdscr.addstr(row, 2, f"
|
|
308
|
+
stdscr.addstr(row, 2, f" {text:<70} ", curses.A_BOLD)
|
|
327
309
|
else:
|
|
328
|
-
stdscr.addstr(row, 2, f"
|
|
310
|
+
stdscr.addstr(row, 2, f" {text:<70} ")
|
|
329
311
|
|
|
330
312
|
stdscr.refresh()
|
|
331
313
|
|
|
332
|
-
# Leer
|
|
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):
|
|
320
|
+
if key in (ord('q'), ord('Q'), 27): # Q o ESC
|
|
339
321
|
break
|
|
340
|
-
elif key in (ord('g'), ord('G')):
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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("\
|
|
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 = "
|
|
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("\
|
|
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
|
|
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
|
-
|
|
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
|
|
391
|
+
# Si se pasan argumentos, procesa actualización por CLI no-interactiva rápida
|
|
410
392
|
if args:
|
|
411
393
|
query = args[0]
|
|
412
|
-
|
|
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
|
-
|
|
401
|
+
selected_model = matches[0]
|
|
422
402
|
else:
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
433
|
-
config["global"] = global_model_selected
|
|
409
|
+
config["global"] = selected_model
|
|
434
410
|
apply_and_exit(config)
|
|
435
411
|
else:
|
|
436
|
-
#
|
|
437
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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": "
|
|
103
|
+
"purpose": "Busca y obtiene ejemplos de uso de iconos de Lucide React."
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|