zugzbot 1.0.6 → 1.0.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.
- package/.opencode/plugins/sdd-bridge.ts +83 -1
- package/.utils/docs_opencode/acp.md +165 -0
- package/.utils/docs_opencode/acp.pdf +0 -0
- package/.utils/docs_opencode/agents.md +803 -0
- package/.utils/docs_opencode/agents.pdf +0 -0
- package/.utils/docs_opencode/commands.md +354 -0
- package/.utils/docs_opencode/commands.pdf +0 -0
- package/.utils/docs_opencode/custom-tools.md +209 -0
- package/.utils/docs_opencode/custom-tools.pdf +0 -0
- package/.utils/docs_opencode/ecosystem.md +81 -0
- package/.utils/docs_opencode/ecosystem.pdf +0 -0
- package/.utils/docs_opencode/formatters.md +142 -0
- package/.utils/docs_opencode/formatters.pdf +0 -0
- package/.utils/docs_opencode/keybinds.md +205 -0
- package/.utils/docs_opencode/keybinds.pdf +0 -0
- package/.utils/docs_opencode/lsp.md +202 -0
- package/.utils/docs_opencode/lsp.pdf +0 -0
- package/.utils/docs_opencode/mcp-servers.md +565 -0
- package/.utils/docs_opencode/mcp-servers.pdf +0 -0
- package/.utils/docs_opencode/models.md +234 -0
- package/.utils/docs_opencode/models.pdf +0 -0
- package/.utils/docs_opencode/permissions.md +248 -0
- package/.utils/docs_opencode/permissions.pdf +0 -0
- package/.utils/docs_opencode/plugins.md +409 -0
- package/.utils/docs_opencode/plugins.pdf +0 -0
- package/.utils/docs_opencode/rules.md +189 -0
- package/.utils/docs_opencode/rules.pdf +0 -0
- package/.utils/docs_opencode/sdk.md +522 -0
- package/.utils/docs_opencode/sdk.pdf +0 -0
- package/.utils/docs_opencode/server.md +324 -0
- package/.utils/docs_opencode/server.pdf +0 -0
- package/.utils/docs_opencode/skills.md +235 -0
- package/.utils/docs_opencode/skills.pdf +0 -0
- package/.utils/docs_opencode/themes.md +378 -0
- package/.utils/docs_opencode/themes.pdf +0 -0
- package/.utils/docs_opencode/tools.md +364 -0
- package/.utils/docs_opencode/tools.pdf +0 -0
- package/.utils/export_opencode_session.py +242 -0
- package/.utils/toggle_model.py +441 -0
- package/README.md +39 -0
- package/bin/init.js +25 -6
- package/models.json +8 -0
- package/opencode.json +4 -5
- package/package.json +3 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import curses
|
|
7
|
+
|
|
8
|
+
# ==============================================================================
|
|
9
|
+
# LISTA DE MODELOS DISPONIBLES EN OPENCODE
|
|
10
|
+
# ==============================================================================
|
|
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
|
+
AGENTS_LIST = ["sdd-orchestrator", "sdd-spec-writer", "sdd-coder", "sdd-tester", "sdd-deployer"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_paths():
|
|
59
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
60
|
+
project_root = os.path.dirname(script_dir)
|
|
61
|
+
opencode_json = os.path.join(project_root, "opencode.json")
|
|
62
|
+
agents_dir = os.path.join(project_root, ".opencode", "agents")
|
|
63
|
+
models_json = os.path.join(project_root, "models.json")
|
|
64
|
+
return opencode_json, agents_dir, models_json
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_models_config():
|
|
68
|
+
opencode_json, agents_dir, models_json = get_paths()
|
|
69
|
+
default_config = {
|
|
70
|
+
"global": "deepseek/deepseek-v4-flash",
|
|
71
|
+
"sdd-orchestrator": "",
|
|
72
|
+
"sdd-spec-writer": "",
|
|
73
|
+
"sdd-coder": "",
|
|
74
|
+
"sdd-tester": "",
|
|
75
|
+
"sdd-deployer": ""
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if os.path.exists(models_json):
|
|
79
|
+
try:
|
|
80
|
+
with open(models_json, 'r', encoding='utf-8') as f:
|
|
81
|
+
return json.load(f)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Guardamos el default si no existe
|
|
86
|
+
try:
|
|
87
|
+
with open(models_json, 'w', encoding='utf-8') as f:
|
|
88
|
+
json.dump(default_config, f, indent=2)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
return default_config
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def save_models_config(config):
|
|
95
|
+
opencode_json, agents_dir, models_json = get_paths()
|
|
96
|
+
try:
|
|
97
|
+
with open(models_json, 'w', encoding='utf-8') as f:
|
|
98
|
+
json.dump(config, f, indent=2)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Error guardando models.json: {e}", file=sys.stderr)
|
|
101
|
+
|
|
102
|
+
|
|
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
|
+
def apply_model(agent_models):
|
|
110
|
+
opencode_json, agents_dir, models_json = get_paths()
|
|
111
|
+
|
|
112
|
+
if not os.path.exists(opencode_json):
|
|
113
|
+
print(f"Error: No se encontró {opencode_json}", file=sys.stderr)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
|
|
116
|
+
# 1. Actualizar opencode.json
|
|
117
|
+
with open(opencode_json, 'r', encoding='utf-8') as f:
|
|
118
|
+
data = json.load(f)
|
|
119
|
+
|
|
120
|
+
agents_config = data.get("agent", {})
|
|
121
|
+
for agent_name, agent_cfg in agents_config.items():
|
|
122
|
+
if agent_name in agent_models:
|
|
123
|
+
agent_cfg["model"] = agent_models[agent_name]
|
|
124
|
+
|
|
125
|
+
with open(opencode_json, 'w', encoding='utf-8') as f:
|
|
126
|
+
json.dump(data, f, indent=2)
|
|
127
|
+
print(" -> opencode.json actualizado con éxito.")
|
|
128
|
+
|
|
129
|
+
# 2. Actualizar frontmatter de agentes (.md)
|
|
130
|
+
if os.path.exists(agents_dir):
|
|
131
|
+
for filename in os.listdir(agents_dir):
|
|
132
|
+
if filename.endswith(".md"):
|
|
133
|
+
# Quitar extensión .md para comparar con el nombre del agente
|
|
134
|
+
agent_name = filename[:-3]
|
|
135
|
+
if agent_name in agent_models:
|
|
136
|
+
target_model = agent_models[agent_name]
|
|
137
|
+
filepath = os.path.join(agents_dir, filename)
|
|
138
|
+
|
|
139
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
140
|
+
content = f.read()
|
|
141
|
+
|
|
142
|
+
frontmatter_pattern = re.compile(
|
|
143
|
+
r"^---$(.*?)^---$", re.MULTILINE | re.DOTALL
|
|
144
|
+
)
|
|
145
|
+
match = frontmatter_pattern.search(content)
|
|
146
|
+
if match:
|
|
147
|
+
frontmatter = match.group(1)
|
|
148
|
+
if "model:" in frontmatter:
|
|
149
|
+
new_frontmatter = re.sub(
|
|
150
|
+
r"^model:.*?$",
|
|
151
|
+
f"model: {target_model}",
|
|
152
|
+
frontmatter,
|
|
153
|
+
flags=re.MULTILINE,
|
|
154
|
+
)
|
|
155
|
+
new_content = content.replace(
|
|
156
|
+
frontmatter, new_frontmatter, 1
|
|
157
|
+
)
|
|
158
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
159
|
+
f.write(new_content)
|
|
160
|
+
print(f" -> {filename} actualizado a: {target_model}")
|
|
161
|
+
else:
|
|
162
|
+
print(f"Advertencia: Directorio de agentes {agents_dir} no existe.")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def print_available_models():
|
|
166
|
+
print("\nModelos disponibles:")
|
|
167
|
+
for m in AVAILABLE_MODELS:
|
|
168
|
+
print(f" - {m}")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ==============================================================================
|
|
173
|
+
# INTERFAZ INTERACTIVA TUI (CURSES)
|
|
174
|
+
# ==============================================================================
|
|
175
|
+
|
|
176
|
+
def select_model_tui(stdscr, title, current_val, is_global=False):
|
|
177
|
+
# Habilitar modo keypad para recibir flechas de cursor
|
|
178
|
+
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
|
|
185
|
+
|
|
186
|
+
query = ""
|
|
187
|
+
selected_idx = 0
|
|
188
|
+
|
|
189
|
+
while True:
|
|
190
|
+
stdscr.clear()
|
|
191
|
+
|
|
192
|
+
# Obtener lista de modelos y añadir "Heredar del Global" si no es la config global
|
|
193
|
+
options = []
|
|
194
|
+
if not is_global:
|
|
195
|
+
options.append("") # Representa [Heredar del Global]
|
|
196
|
+
|
|
197
|
+
# Filtrar modelos según query
|
|
198
|
+
filtered_models = [m for m in AVAILABLE_MODELS if query.lower() in m.lower()]
|
|
199
|
+
options.extend(filtered_models)
|
|
200
|
+
|
|
201
|
+
# Validar límites de selección
|
|
202
|
+
if selected_idx >= len(options):
|
|
203
|
+
selected_idx = max(0, len(options) - 1)
|
|
204
|
+
|
|
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)
|
|
209
|
+
|
|
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)
|
|
214
|
+
|
|
215
|
+
# Mostrar opciones con paginación simple de terminal
|
|
216
|
+
start_row = 8
|
|
217
|
+
max_rows = curses.LINES - start_row - 2
|
|
218
|
+
|
|
219
|
+
if not options:
|
|
220
|
+
stdscr.addstr(start_row, 4, "No hay modelos que coincidan con la búsqueda.", curses.color_pair(2))
|
|
221
|
+
else:
|
|
222
|
+
# Mostrar solo lo que quepa en la pantalla
|
|
223
|
+
for i in range(min(len(options), max_rows)):
|
|
224
|
+
opt = options[i]
|
|
225
|
+
row = start_row + i
|
|
226
|
+
|
|
227
|
+
# Formatear el label
|
|
228
|
+
if opt == "":
|
|
229
|
+
label = "[ Heredar del Global ]"
|
|
230
|
+
else:
|
|
231
|
+
label = opt
|
|
232
|
+
|
|
233
|
+
# Indicar si es el activo actual
|
|
234
|
+
if opt == current_val:
|
|
235
|
+
label += " (activo)"
|
|
236
|
+
|
|
237
|
+
# Resaltar la opción seleccionada
|
|
238
|
+
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))
|
|
242
|
+
else:
|
|
243
|
+
stdscr.addstr(row, 2, f" {label:<50}")
|
|
244
|
+
|
|
245
|
+
stdscr.refresh()
|
|
246
|
+
|
|
247
|
+
# Leer tecla
|
|
248
|
+
try:
|
|
249
|
+
key = stdscr.getch()
|
|
250
|
+
except KeyboardInterrupt:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
if key in (27, curses.KEY_CANCEL): # ESC
|
|
254
|
+
return None
|
|
255
|
+
elif key in (10, 13, curses.KEY_ENTER): # Enter
|
|
256
|
+
if options:
|
|
257
|
+
return options[selected_idx]
|
|
258
|
+
return None
|
|
259
|
+
elif key == curses.KEY_UP:
|
|
260
|
+
if selected_idx > 0:
|
|
261
|
+
selected_idx -= 1
|
|
262
|
+
elif key == curses.KEY_DOWN:
|
|
263
|
+
if selected_idx < len(options) - 1:
|
|
264
|
+
selected_idx += 1
|
|
265
|
+
elif key in (127, 8, curses.KEY_BACKSPACE): # Backspace
|
|
266
|
+
query = query[:-1]
|
|
267
|
+
selected_idx = 0
|
|
268
|
+
elif 32 <= key <= 126: # Carácter imprimible
|
|
269
|
+
query += chr(key)
|
|
270
|
+
selected_idx = 0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main_tui(stdscr):
|
|
274
|
+
# Habilitar keypad
|
|
275
|
+
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
|
|
283
|
+
|
|
284
|
+
config = load_models_config()
|
|
285
|
+
selected_row = 0
|
|
286
|
+
|
|
287
|
+
while True:
|
|
288
|
+
stdscr.clear()
|
|
289
|
+
|
|
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)
|
|
294
|
+
|
|
295
|
+
# Construir items del menú interactivo
|
|
296
|
+
global_val = config.get("global", "deepseek/deepseek-v4-flash")
|
|
297
|
+
|
|
298
|
+
menu_items = [
|
|
299
|
+
("global", f"Modelo Global: {global_val}"),
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
for agent in AGENTS_LIST:
|
|
303
|
+
val = config.get(agent, "")
|
|
304
|
+
label = val if val else f"[Hereda del global: {global_val}]"
|
|
305
|
+
menu_items.append((agent, f"{agent}: {label}"))
|
|
306
|
+
|
|
307
|
+
# 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"))
|
|
310
|
+
|
|
311
|
+
start_row = 6
|
|
312
|
+
for idx, (key, text) in enumerate(menu_items):
|
|
313
|
+
row = start_row + idx
|
|
314
|
+
|
|
315
|
+
# Resaltar la opción actual
|
|
316
|
+
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))
|
|
320
|
+
else:
|
|
321
|
+
if key == "save":
|
|
322
|
+
stdscr.addstr(row, 2, f" {text}", curses.color_pair(3) | curses.A_BOLD)
|
|
323
|
+
elif key == "cancel":
|
|
324
|
+
stdscr.addstr(row, 2, f" {text}", curses.color_pair(2))
|
|
325
|
+
elif key == "global":
|
|
326
|
+
stdscr.addstr(row, 2, f" {text}", curses.color_pair(3))
|
|
327
|
+
else:
|
|
328
|
+
stdscr.addstr(row, 2, f" {text}")
|
|
329
|
+
|
|
330
|
+
stdscr.refresh()
|
|
331
|
+
|
|
332
|
+
# Leer acción del usuario
|
|
333
|
+
try:
|
|
334
|
+
key = stdscr.getch()
|
|
335
|
+
except KeyboardInterrupt:
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
if key in (ord('q'), ord('Q'), 27): # Q o ESC
|
|
339
|
+
break
|
|
340
|
+
elif key in (ord('g'), ord('G')): # G de Guardar
|
|
341
|
+
apply_and_exit(config)
|
|
342
|
+
break
|
|
343
|
+
elif key == curses.KEY_UP:
|
|
344
|
+
if selected_row > 0:
|
|
345
|
+
selected_row -= 1
|
|
346
|
+
elif key == curses.KEY_DOWN:
|
|
347
|
+
if selected_row < len(menu_items) - 1:
|
|
348
|
+
selected_row += 1
|
|
349
|
+
elif key in (10, 13, curses.KEY_ENTER):
|
|
350
|
+
# Obtener el item seleccionado
|
|
351
|
+
action_key, _ = menu_items[selected_row]
|
|
352
|
+
|
|
353
|
+
if action_key == "save":
|
|
354
|
+
apply_and_exit(config)
|
|
355
|
+
break
|
|
356
|
+
elif action_key == "cancel":
|
|
357
|
+
break
|
|
358
|
+
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)
|
|
361
|
+
if new_val is not None:
|
|
362
|
+
config["global"] = new_val
|
|
363
|
+
save_models_config(config)
|
|
364
|
+
else:
|
|
365
|
+
# Editar un agente específico
|
|
366
|
+
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)
|
|
368
|
+
if new_val is not None:
|
|
369
|
+
config[action_key] = new_val
|
|
370
|
+
save_models_config(config)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def apply_and_exit(config):
|
|
374
|
+
# Guardar la configuración
|
|
375
|
+
save_models_config(config)
|
|
376
|
+
|
|
377
|
+
# Aplicar el mapeo de modelos final en cascada
|
|
378
|
+
global_model = config.get("global", "deepseek/deepseek-v4-flash")
|
|
379
|
+
final_agent_models = {}
|
|
380
|
+
|
|
381
|
+
print("\nMapeo de modelos resultante:")
|
|
382
|
+
for agent_name in AGENTS_LIST:
|
|
383
|
+
custom_val = config.get(agent_name, "")
|
|
384
|
+
final_model = custom_val if custom_val else global_model
|
|
385
|
+
final_agent_models[agent_name] = final_model
|
|
386
|
+
origin = "personalizado" if custom_val else "global"
|
|
387
|
+
print(f" * {agent_name} -> {final_model} ({origin})")
|
|
388
|
+
|
|
389
|
+
print("\nAplicando cambios...")
|
|
390
|
+
apply_model(final_agent_models)
|
|
391
|
+
print("\nModelos configurados exitosamente! 🎉")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ==============================================================================
|
|
395
|
+
# MAIN ENTRY POINT
|
|
396
|
+
# ==============================================================================
|
|
397
|
+
|
|
398
|
+
def main():
|
|
399
|
+
args = sys.argv[1:]
|
|
400
|
+
|
|
401
|
+
# Cargar la configuración actual del JSON
|
|
402
|
+
config = load_models_config()
|
|
403
|
+
|
|
404
|
+
# Si se pide listar modelos por CLI
|
|
405
|
+
if args and args[0] in ("--list", "-l", "list"):
|
|
406
|
+
print_available_models()
|
|
407
|
+
sys.exit(0)
|
|
408
|
+
|
|
409
|
+
# Si se pasan argumentos por parámetro, corre en modo CLI no-interactivo rápido
|
|
410
|
+
if args:
|
|
411
|
+
query = args[0]
|
|
412
|
+
matches = search_model(query)
|
|
413
|
+
|
|
414
|
+
if not matches:
|
|
415
|
+
print(
|
|
416
|
+
f"Error: Ningún modelo coincide con '{query}'. Use 'list' para ver todos.",
|
|
417
|
+
file=sys.stderr,
|
|
418
|
+
)
|
|
419
|
+
sys.exit(1)
|
|
420
|
+
elif len(matches) == 1:
|
|
421
|
+
global_model_selected = matches[0]
|
|
422
|
+
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}':")
|
|
426
|
+
for m in matches:
|
|
427
|
+
print(f" * {m}")
|
|
428
|
+
print(
|
|
429
|
+
f"Seleccionando automáticamente la primera: {global_model_selected}\n"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Actualizar el global_model en config y persistir
|
|
433
|
+
config["global"] = global_model_selected
|
|
434
|
+
apply_and_exit(config)
|
|
435
|
+
else:
|
|
436
|
+
# Si NO se pasan argumentos, inicia la TUI interactiva
|
|
437
|
+
curses.wrapper(main_tui)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
main()
|
package/README.md
CHANGED
|
@@ -85,6 +85,45 @@ Para evitar la pérdida de contexto entre turnos y agentes, el arnés implementa
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
+
## 🎯 Configuración Rápida de Modelos de IA
|
|
89
|
+
|
|
90
|
+
El arnés de Zugzbot incluye un sistema de asignación de modelos centralizado, ágil e interactivo. La configuración se almacena en el archivo `models.json` en la raíz de tu proyecto.
|
|
91
|
+
|
|
92
|
+
### 1. El Archivo `models.json`
|
|
93
|
+
El formato de este archivo es muy simple e intuitivo:
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"global": "deepseek/deepseek-v4-flash",
|
|
97
|
+
"sdd-orchestrator": "",
|
|
98
|
+
"sdd-spec-writer": "",
|
|
99
|
+
"sdd-coder": "google/gemini-2.5-pro",
|
|
100
|
+
"sdd-tester": "",
|
|
101
|
+
"sdd-deployer": ""
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
* **Modelo Global**: Define el modelo predeterminado (`"global"`) que usarán los agentes por defecto.
|
|
105
|
+
* **Heredar del Global**: Si dejas el modelo de un agente vacío (`""`), el sistema le asignará automáticamente el modelo `"global"`.
|
|
106
|
+
* **Personalización**: Si quieres que un agente en particular use un modelo diferente (ej. un modelo de razonamiento o de programación más potente), escribe el identificador del modelo directamente en su clave.
|
|
107
|
+
|
|
108
|
+
### 2. Sincronización Automática
|
|
109
|
+
¡No tienes que hacer nada manual! Al abrir la interfaz de **OpenCode**, el plugin integrado detecta y lee de forma automática tu `models.json`, actualizando instantáneamente `opencode.json` y los markdown de tus agentes en disco con total transparencia.
|
|
110
|
+
|
|
111
|
+
### 3. Interfaz Visual en Terminal (TUI)
|
|
112
|
+
Para cambiar de modelos de forma cómoda y sin riesgo de cometer errores tipográficos en los identificadores de IA, puedes abrir la **interfaz visual interactiva** en tu terminal.
|
|
113
|
+
|
|
114
|
+
Ejecuta el script sin argumentos:
|
|
115
|
+
```bash
|
|
116
|
+
python3 .utils/toggle_model.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
* **Menú Principal**: Muévete con las flechas `[↑/↓]` y presiona `[Enter]` para editar el modelo global o el de un agente específico.
|
|
120
|
+
* **Selector con Buscador**: Dentro del selector de modelos, puedes empezar a escribir para filtrar la lista en tiempo real por palabra clave (ej. *"gemini"* o *"deepseek"*).
|
|
121
|
+
* **Guardar y Aplicar**: Selecciona `[✓ GUARDAR Y APLICAR CAMBIOS]` o pulsa la tecla `[G]` para persistir la configuración e inyectarla en todos tus agentes al instante.
|
|
122
|
+
|
|
123
|
+
*Nota: Sigue siendo compatible con la CLI tradicional rápida. Por ejemplo, `python3 .utils/toggle_model.py google/gemini-3.5-flash` actualizará de inmediato tu modelo global en modo lote no interactivo.*
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
88
127
|
## 🔄 Modo Autopiloto (/loop)
|
|
89
128
|
|
|
90
129
|
El arnés de Zugzbot viene preparado de forma nativa para soportar el **Modo Autopiloto** mediante el comando `/loop` en OpenCode.
|
package/bin/init.js
CHANGED
|
@@ -15,14 +15,28 @@ const yellow = '\x1b[33m';
|
|
|
15
15
|
const red = '\x1b[31m';
|
|
16
16
|
const bold = '\x1b[1m';
|
|
17
17
|
|
|
18
|
-
console.log(`\n${bold}${cyan}🤖 Zugzbot Harness Installer${reset}\n`);
|
|
19
|
-
|
|
20
18
|
const pkgRoot = join(__dirname, '..');
|
|
21
19
|
const targetDir = process.cwd();
|
|
22
20
|
|
|
21
|
+
const banner = `
|
|
22
|
+
${bold}${cyan}███████╗██╗ ██╗ ██████╗ ███████╗
|
|
23
|
+
╚══███╔╝██║ ██║██╔════╝ ╚══███╔╝
|
|
24
|
+
███╔╝ ██║ ██║██║ ███╗ ███╔╝
|
|
25
|
+
███╔╝ ██║ ██║██║ ██║ ███╔╝
|
|
26
|
+
███████╗╚██████╔╝╚██████╔╝███████╗
|
|
27
|
+
╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝${reset}
|
|
28
|
+
${bold}${yellow} Harness Installer v1.0.7${reset}\n`;
|
|
29
|
+
|
|
30
|
+
console.log(banner);
|
|
31
|
+
console.log(`${bold}${cyan}🔍 Detectando entorno de trabajo...${reset}`);
|
|
32
|
+
console.log(` ${green}✔ Directorio destino identificado: ${targetDir}${reset}\n`);
|
|
33
|
+
console.log(`${bold}${cyan}📦 Instalando arnés de desarrollo SDD...${reset}`);
|
|
34
|
+
|
|
23
35
|
// Define items to copy
|
|
24
36
|
const itemsToCopy = [
|
|
25
37
|
{ name: '.opencode', src: join(pkgRoot, '.opencode'), dest: join(targetDir, '.opencode'), type: 'dir' },
|
|
38
|
+
{ name: '.utils', src: join(pkgRoot, '.utils'), dest: join(targetDir, '.utils'), type: 'dir' },
|
|
39
|
+
{ name: 'models.json', src: join(pkgRoot, 'models.json'), dest: join(targetDir, 'models.json'), type: 'file' },
|
|
26
40
|
{ name: 'opencode.json', src: join(pkgRoot, 'opencode.json'), dest: join(targetDir, 'opencode.json'), type: 'file' },
|
|
27
41
|
{ name: 'tui.json', src: join(pkgRoot, 'tui.json'), dest: join(targetDir, 'tui.json'), type: 'file' }
|
|
28
42
|
];
|
|
@@ -47,11 +61,16 @@ for (const item of itemsToCopy) {
|
|
|
47
61
|
|
|
48
62
|
const relativeDest = relative(targetDir, item.dest);
|
|
49
63
|
|
|
64
|
+
if (item.src === item.dest) {
|
|
65
|
+
copiedCount++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
try {
|
|
51
70
|
if (fs.existsSync(item.dest)) {
|
|
52
|
-
console.log(
|
|
71
|
+
console.log(` ${yellow}✔ Updated existing: ${relativeDest}${reset}`);
|
|
53
72
|
} else {
|
|
54
|
-
console.log(
|
|
73
|
+
console.log(` ${green}✔ Copied: ${relativeDest}${reset}`);
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
if (item.type === 'dir') {
|
|
@@ -87,8 +106,8 @@ try {
|
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
if (copiedCount > 0) {
|
|
90
|
-
console.log(`\n${bold}${green}✨ Zugzbot
|
|
91
|
-
console.log(
|
|
109
|
+
console.log(`\n${bold}${green}✨ ¡Arnés de Zugzbot instalado y actualizado con éxito!${reset}`);
|
|
110
|
+
console.log(`🚀 Ejecuta ${bold}${cyan}opencode${reset} para iniciar tu sesión de desarrollo autónomo.\n`);
|
|
92
111
|
} else {
|
|
93
112
|
console.log(`\n${red}❌ No files were installed.${reset}\n`);
|
|
94
113
|
}
|
package/models.json
ADDED
package/opencode.json
CHANGED
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"lsp": true,
|
|
23
|
-
"instructions": [
|
|
24
|
-
],
|
|
23
|
+
"instructions": [],
|
|
25
24
|
"plugin": [
|
|
26
25
|
"opencode-dynamic-context-pruning",
|
|
27
26
|
"opencode-notificator"
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
"mcp"
|
|
46
45
|
],
|
|
47
46
|
"enabled": true,
|
|
48
|
-
"purpose": "
|
|
47
|
+
"purpose": "Librería UI objetivo. Usar para browse/search/install de componentes y blocks (dashboards, landings, auth, forms)."
|
|
49
48
|
},
|
|
50
49
|
"context7": {
|
|
51
50
|
"type": "local",
|
|
@@ -67,7 +66,7 @@
|
|
|
67
66
|
"--isolated"
|
|
68
67
|
],
|
|
69
68
|
"enabled": true,
|
|
70
|
-
"purpose": "Playwright MCP. Artefactos (screenshots, traces, videos, downloads) van a .openspec/.playwright/<call-id>/. Para evidencia por
|
|
69
|
+
"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."
|
|
71
70
|
},
|
|
72
71
|
"next-devtools": {
|
|
73
72
|
"type": "local",
|
|
@@ -91,7 +90,7 @@
|
|
|
91
90
|
"--stdio"
|
|
92
91
|
],
|
|
93
92
|
"enabled": true,
|
|
94
|
-
"purpose": "Permite buscar, filtrar y obtener ejemplos de uso JSX de
|
|
93
|
+
"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."
|
|
95
94
|
}
|
|
96
95
|
},
|
|
97
96
|
"agent": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zugzbot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
".opencode/",
|
|
12
|
+
".utils/",
|
|
13
|
+
"models.json",
|
|
12
14
|
"opencode.json",
|
|
13
15
|
"tui.json"
|
|
14
16
|
],
|