@innvisor/conny-ai 9.7.0

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.
Files changed (175) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +369 -0
  5. package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
  6. package/brand-assets/Conny.web.logo.png +0 -0
  7. package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
  8. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
  9. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
  10. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
  11. package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
  12. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
  13. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
  14. package/brand-assets/conny-demo/manifest.json +22 -0
  15. package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
  16. package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
  17. package/brand-assets/conny-logo.png +0 -0
  18. package/brand-assets/web.background.png +0 -0
  19. package/brand_assets.py +323 -0
  20. package/conny +28 -0
  21. package/conny-chat.py +579 -0
  22. package/conny-omni.py +3843 -0
  23. package/conny.py +113 -0
  24. package/conny_agents/__init__.py +1 -0
  25. package/conny_agents/agenda.py +1 -0
  26. package/conny_agents/captacion.py +1 -0
  27. package/conny_agents/conocimiento.py +1 -0
  28. package/conny_agents/escalacion.py +1 -0
  29. package/conny_agents/objeciones.py +1 -0
  30. package/conny_agents/seguimiento.py +1 -0
  31. package/conny_app.py +287 -0
  32. package/conny_audio.py +350 -0
  33. package/conny_audio_learn.py +84 -0
  34. package/conny_brain_v10.py +804 -0
  35. package/conny_bridge.py +656 -0
  36. package/conny_calendar.py +169 -0
  37. package/conny_cli.py +11784 -0
  38. package/conny_cli_bb.py +437 -0
  39. package/conny_commands.py +243 -0
  40. package/conny_config.py +215 -0
  41. package/conny_core/__init__.py +3 -0
  42. package/conny_core/conversation_engine.py +446 -0
  43. package/conny_core/first_turn_ops.py +287 -0
  44. package/conny_core/persona_registry.py +157 -0
  45. package/conny_core/prompt_ops.py +561 -0
  46. package/conny_cron.py +72 -0
  47. package/conny_demo_v2.py +209 -0
  48. package/conny_demo_voice.py +134 -0
  49. package/conny_design.py +43 -0
  50. package/conny_doctor.py +319 -0
  51. package/conny_domino.py +696 -0
  52. package/conny_generator.py +447 -0
  53. package/conny_google_auth.py +159 -0
  54. package/conny_i18n.py +619 -0
  55. package/conny_init.py +509 -0
  56. package/conny_integrations/__init__.py +4 -0
  57. package/conny_integrations/llm.py +1 -0
  58. package/conny_integrations/vault.py +77 -0
  59. package/conny_integrations/whatsapp.py +1 -0
  60. package/conny_intelligence.py +65 -0
  61. package/conny_learning.py +154 -0
  62. package/conny_memory.py +243 -0
  63. package/conny_memory_engine.py +292 -0
  64. package/conny_nova_proxy.py +170 -0
  65. package/conny_nuke_robot_phrases.py +493 -0
  66. package/conny_pairing.py +253 -0
  67. package/conny_patch.py +291 -0
  68. package/conny_persona_cli.py +150 -0
  69. package/conny_router.py +308 -0
  70. package/conny_runtime_ops.py +271 -0
  71. package/conny_session.py +516 -0
  72. package/conny_skills/__init__.py +1 -0
  73. package/conny_skills/demo_mode.py +35 -0
  74. package/conny_skills/text_processing.py +1 -0
  75. package/conny_skills/tone_detection.py +1 -0
  76. package/conny_smart_features.py +333 -0
  77. package/conny_studio.py +161 -0
  78. package/conny_sync_fix.py +306 -0
  79. package/conny_tui.py +512 -0
  80. package/conny_tui_select.py +202 -0
  81. package/conny_ultra_config.py +411 -0
  82. package/conny_uncertainty.py +174 -0
  83. package/conny_utils.py +87 -0
  84. package/conny_voice.py +156 -0
  85. package/conny_voice_engine.py +124 -0
  86. package/conny_web_search.py +66 -0
  87. package/conny_weekly_report.py +85 -0
  88. package/conny_worm.py +88 -0
  89. package/core/__init__.py +25 -0
  90. package/ecosystem.config.js +24 -0
  91. package/fix_init.py +27 -0
  92. package/install.sh +78 -0
  93. package/knowledge_base.py +330 -0
  94. package/nova/rules/default.yaml +37 -0
  95. package/nova_bridge.py +509 -0
  96. package/npm/conny.js +471 -0
  97. package/package.json +102 -0
  98. package/personas/conny/base/default.yaml +35 -0
  99. package/personas/conny/base/estetica_whatsapp.yaml +36 -0
  100. package/requirements.txt +14 -0
  101. package/run.sh +47 -0
  102. package/search.py +465 -0
  103. package/smart_handoff.py +1150 -0
  104. package/src/__init__.py +0 -0
  105. package/src/conny/__init__.py +0 -0
  106. package/src/conny/admin/__init__.py +0 -0
  107. package/src/conny/admin/api.py +234 -0
  108. package/src/conny/admin/dashboard.py +772 -0
  109. package/src/conny/api/__init__.py +0 -0
  110. package/src/conny/api/routes.py +8851 -0
  111. package/src/conny/brain/__init__.py +15 -0
  112. package/src/conny/brain/engine.py +804 -0
  113. package/src/conny/brain/learning.py +154 -0
  114. package/src/conny/brain/memory.py +324 -0
  115. package/src/conny/brain/smart_features.py +333 -0
  116. package/src/conny/brain/uncertainty.py +167 -0
  117. package/src/conny/channels/__init__.py +0 -0
  118. package/src/conny/channels/audio.py +316 -0
  119. package/src/conny/channels/cli.py +11795 -0
  120. package/src/conny/channels/logo_art.py +11 -0
  121. package/src/conny/channels/voice.py +156 -0
  122. package/src/conny/core/__init__.py +0 -0
  123. package/src/conny/core/config.py +215 -0
  124. package/src/conny/core/cron.py +72 -0
  125. package/src/conny/core/messenger.py +563 -0
  126. package/src/conny/core/router.py +297 -0
  127. package/src/conny/core/session.py +312 -0
  128. package/src/conny/demo/__init__.py +0 -0
  129. package/src/conny/demo/handler.py +3110 -0
  130. package/src/conny/integrations/__init__.py +19 -0
  131. package/src/conny/integrations/calendar.py +169 -0
  132. package/src/conny/integrations/knowledge.py +312 -0
  133. package/src/conny/integrations/search.py +66 -0
  134. package/src/conny/personas/__init__.py +0 -0
  135. package/src/conny/personas/generator.py +447 -0
  136. package/src/conny/production/__init__.py +0 -0
  137. package/src/conny/production/domino.py +696 -0
  138. package/src/conny/production/guard.py +550 -0
  139. package/src/conny/production/handoff.py +1150 -0
  140. package/src/conny/production/monitor.py +353 -0
  141. package/src/conny/utils/__init__.py +2 -0
  142. package/src/conny/utils/helpers.py +75 -0
  143. package/src/conny/utils/i18n.py +619 -0
  144. package/src/core/admin_engines.py +772 -0
  145. package/src/core/globals.py +11845 -0
  146. package/src/core/orchestrator.py +273 -0
  147. package/src/core/production_monitor.py +353 -0
  148. package/src/core/runtime.py +5487 -0
  149. package/src/domain/onboarding_flow.py +230 -0
  150. package/src/domain/prompts/__init__.py +1 -0
  151. package/src/domain/prompts/prospect_pitch.py +282 -0
  152. package/src/domain/send_guard.py +636 -0
  153. package/src/domain/swarm/queen.py +96 -0
  154. package/src/infrastructure/llm_providers/engine.py +487 -0
  155. package/src/interfaces/mcp_server.py +73 -0
  156. package/src/interfaces/nova_bridge.py +58 -0
  157. package/src/interfaces/web/admin_api.py +1379 -0
  158. package/src/interfaces/web/app.py +9408 -0
  159. package/src/interfaces/web/demo_handler.py +3450 -0
  160. package/src/interfaces/web/static/generate_avatars.py +46 -0
  161. package/v7/__init__.py +46 -0
  162. package/v7/agents/__init__.py +46 -0
  163. package/v7/agents/agenda.py +77 -0
  164. package/v7/agents/base.py +216 -0
  165. package/v7/agents/captacion.py +60 -0
  166. package/v7/agents/conocimiento.py +69 -0
  167. package/v7/agents/escalacion.py +83 -0
  168. package/v7/agents/objeciones.py +109 -0
  169. package/v7/agents/seguimiento.py +71 -0
  170. package/v7/memory/__init__.py +46 -0
  171. package/v7/memory/patient_profile.py +200 -0
  172. package/v7/orchestrator.py +275 -0
  173. package/v7/postprocess.py +127 -0
  174. package/v7/router.py +239 -0
  175. package/verify_conversation_impl.py +48 -0
package/conny_tui.py ADDED
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ conny_tui — Interfaz interactiva de Conny con flechas ↑↓
4
+
5
+ Reemplaza el texto de ayuda estático por un menú navegable.
6
+ Se activa cuando el usuario corre 'conny' sin argumentos.
7
+
8
+ Instalar:
9
+ Copia este archivo a ~/conny/conny_tui.py
10
+ El parche en conny_sync_fix.py lo activa automáticamente.
11
+
12
+ Uso directo:
13
+ python3 conny_tui.py
14
+ """
15
+
16
+ import curses
17
+ import os
18
+ import sys
19
+ import json
20
+ import subprocess
21
+ import time
22
+ import shutil
23
+ from pathlib import Path
24
+ from typing import List, Optional, Tuple
25
+
26
+ # ── Config (mismos defaults que conny_cli.py) ─────────────────────────────
27
+ VERSION = "8.0.2"
28
+ CONNY_HOME = os.getenv("CONNY_HOME", str(Path.home() / ".conny"))
29
+ CONNY_DIR = os.getenv("CONNY_DIR", str(Path(__file__).resolve().parent))
30
+ INSTANCES_DIR = os.getenv("INSTANCES_DIR", str(Path.home() / "conny-instances"))
31
+ CLI_SCRIPT = os.getenv("CONNY_CLI", str(Path(__file__).resolve().parent / "conny_cli.py"))
32
+
33
+ # ── Colores curses (índices de par) ─────────────────────────────────────────
34
+ CP_LOGO1 = 1 # Rosa — letras CONNY (gradiente línea 1)
35
+ CP_LOGO2 = 10 # Rosa claro (gradiente línea 2)
36
+ CP_LOGO3 = 11 # Lila (gradiente línea 3)
37
+ CP_LOGO4 = 12 # Púrpura (gradiente línea 4)
38
+ CP_LOGO5 = 13 # Púrpura-azul (gradiente línea 5)
39
+ CP_LOGO6 = 14 # Lavanda (gradiente línea 6)
40
+ CP_TITLE = 2 # Blanco bold — subtítulo
41
+ CP_DIM = 3 # Gris dim — descripción
42
+ CP_SEL = 4 # Item seleccionado (bg highlight)
43
+ CP_OK = 5 # Verde ✓
44
+ CP_WARN = 6 # Amarillo
45
+ CP_BAR = 7 # Bordes de la caja y separadores
46
+ CP_INST = 8 # Instancia activa
47
+ CP_HINT = 9 # Ayuda de teclas
48
+
49
+
50
+ # ── Estructura de instancia ──────────────────────────────────────────────────
51
+ class Instance:
52
+ def __init__(self, name: str, label: str, port: int, online: bool,
53
+ sector: str = "otro", path: str = "", is_base: bool = False):
54
+ self.name = name
55
+ self.label = label
56
+ self.port = port
57
+ self.online = online
58
+ self.sector = sector
59
+ self.path = path
60
+ self.is_base = is_base
61
+
62
+ @property
63
+ def status_char(self):
64
+ return "●" if self.online else "○"
65
+
66
+ @property
67
+ def status_str(self):
68
+ return "online" if self.online else "offline"
69
+
70
+
71
+ # ── Menú principal ──────────────────────────────────────────────────────────
72
+ MENU_ITEMS = [
73
+ # (id, emoji, label, descripción, requiere_instancia)
74
+ ("new", "🚀", "Nueva instancia", "Lanza una nueva recepcionista", False),
75
+ ("sync", "🔥", "Sincronizar todo", "Despliega mejoras del core a clientes", False),
76
+ ("list", "📋", "Mis agentes", "Listado de todas tus Connys", False),
77
+ ("dashboard", "📊", "Dashboard", "Panel de control en vivo", False),
78
+ ("health", "🩺", "Health check", "Chequeo rápido de signos vitales", False),
79
+ ("logs", "📜", "Brain Stream", "Stream de pensamientos en vivo", True),
80
+ ("restart", "↺ ", "Reiniciar", "Quick reboot de la instancia", True),
81
+ ("config", "⚙ ", "Configurar", "Ajustes profundos y llaves", True),
82
+ ("status", "🔍", "Estado Vital", "CPU, memoria, webhook y env", True),
83
+ ("doctor", "💊", "Diagnóstico", "Auto-heal y reparaciones", False),
84
+ ("backup", "💾", "Snapshot", "Crea un backup de seguridad", True),
85
+ ("delete", "🗑 ", "Eliminar", "Borrar instancia por completo", True),
86
+ ("_exit", "✕ ", "Salir", "Hasta luego! 👋", False),
87
+ ]
88
+
89
+
90
+ # ── Helpers ──────────────────────────────────────────────────────────────────
91
+
92
+ def _check_port(port: int, timeout: float = 0.4) -> bool:
93
+ """Verificar si hay algo escuchando en el puerto."""
94
+ import socket
95
+ try:
96
+ with socket.create_connection(("127.0.0.1", port), timeout=timeout):
97
+ return True
98
+ except Exception:
99
+ return False
100
+
101
+
102
+ def _load_instances() -> List[Instance]:
103
+ instances: List[Instance] = []
104
+
105
+ # Base instance
106
+ base_env_path = Path(CONNY_DIR) / ".env"
107
+ if base_env_path.exists():
108
+ port = _env_val(base_env_path, "PORT", "8001")
109
+ try:
110
+ port_int = int(port)
111
+ except ValueError:
112
+ port_int = 8001
113
+ online = _check_port(port_int)
114
+ instances.append(Instance(
115
+ name="base",
116
+ label="Base (template)",
117
+ port=port_int,
118
+ online=online,
119
+ path=CONNY_DIR,
120
+ is_base=True,
121
+ ))
122
+
123
+ # Client instances
124
+ inst_root = Path(INSTANCES_DIR)
125
+ if inst_root.is_dir():
126
+ for d in sorted(inst_root.iterdir()):
127
+ if not d.is_dir():
128
+ continue
129
+ env_file = d / ".env"
130
+ if not env_file.exists():
131
+ continue
132
+ port_str = _env_val(env_file, "PORT", "8002")
133
+ try:
134
+ port_int = int(port_str)
135
+ except ValueError:
136
+ port_int = 8002
137
+
138
+ # Cargar metadata de instance.json
139
+ label = d.name.replace("-", " ").title()
140
+ sector = "otro"
141
+ meta_path = d / "instance.json"
142
+ if meta_path.exists():
143
+ try:
144
+ meta = json.loads(meta_path.read_text())
145
+ label = meta.get("label", label)
146
+ sector = meta.get("sector", sector)
147
+ except Exception:
148
+ pass
149
+
150
+ online = _check_port(port_int)
151
+ instances.append(Instance(
152
+ name=d.name,
153
+ label=label,
154
+ port=port_int,
155
+ online=online,
156
+ sector=sector,
157
+ path=str(d),
158
+ is_base=False,
159
+ ))
160
+
161
+ return instances
162
+
163
+
164
+ def _env_val(env_file: Path, key: str, default: str = "") -> str:
165
+ try:
166
+ for line in env_file.read_text(errors="replace").splitlines():
167
+ line = line.strip()
168
+ if line.startswith(f"{key}="):
169
+ return line[len(key) + 1:].strip().strip('"').strip("'")
170
+ except Exception:
171
+ pass
172
+ return default
173
+
174
+
175
+ def _run_conny(*args: str) -> None:
176
+ """Llamar a conny_cli.py con argumentos."""
177
+ curses.endwin()
178
+ print()
179
+ try:
180
+ if Path(CLI_SCRIPT).exists():
181
+ cmd = [sys.executable, CLI_SCRIPT] + list(args)
182
+ else:
183
+ cmd = ["conny"] + list(args)
184
+ subprocess.run(cmd)
185
+ except KeyboardInterrupt:
186
+ print("\n Interrumpido")
187
+ print()
188
+ input(" Presiona Enter para volver al menú... ")
189
+
190
+
191
+ def _ask_instance(instances: List[Instance]) -> Optional[Instance]:
192
+ """Selector simple de instancia (fuera de curses)."""
193
+ clients = [i for i in instances if not i.is_base]
194
+ if not clients:
195
+ print("\n ⚠ No hay instancias de clientes todavía.")
196
+ print(" Usa 'Nueva instancia' para crear una.\n")
197
+ input(" Presiona Enter para volver... ")
198
+ return None
199
+ if len(clients) == 1:
200
+ return clients[0]
201
+ print()
202
+ for idx, inst in enumerate(clients, 1):
203
+ dot = "●" if inst.online else "○"
204
+ print(f" {idx}. {dot} {inst.label} (:{inst.port})")
205
+ print()
206
+ while True:
207
+ try:
208
+ v = input(" Elige número (o Enter para cancelar): ").strip()
209
+ except (EOFError, KeyboardInterrupt):
210
+ return None
211
+ if v == "":
212
+ return None
213
+ if v.isdigit() and 1 <= int(v) <= len(clients):
214
+ return clients[int(v) - 1]
215
+ print(" Escribe el número de la opción")
216
+
217
+
218
+ def _execute_menu_action(action_id: str, instances: List[Instance]) -> None:
219
+ """Ejecutar la acción seleccionada del menú."""
220
+ if action_id == "_exit":
221
+ return
222
+
223
+ needs_instance = next(
224
+ (item[4] for item in MENU_ITEMS if item[0] == action_id), False
225
+ )
226
+
227
+ if needs_instance:
228
+ curses.endwin()
229
+ inst = _ask_instance(instances)
230
+ if inst is None:
231
+ return
232
+ _run_conny(action_id, inst.name)
233
+ else:
234
+ _run_conny(action_id)
235
+
236
+
237
+ # ── Logo — idéntico al banner de `conny init` ──────────────────────────────
238
+ #
239
+ # Cada entrada: (texto, color_pair, bold)
240
+ # CP_LOGO → letras CONNY (lavanda/púrpura)
241
+ # CP_DIM → tagline (gris dim)
242
+ # CP_TITLE → versión / sectores (blanco)
243
+ # CP_BAR → línea separadora (gris medio)
244
+ #
245
+ _SEP_LINE = " ──────────────────────────────────────────────────────"
246
+
247
+ LOGO_LINES = [
248
+ (" ██████╗ ██████╗ ███╗ ██╗███╗ ██╗██╗ ██╗", CP_LOGO1, True),
249
+ (" ██╔════╝ ██╔═══██╗████╗ ██║████╗ ██║╚██╗ ██╔╝", CP_LOGO2, True),
250
+ (" ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║ ╚████╔╝ ", CP_LOGO3, True),
251
+ (" ██║ ██║ ██║██║╚██╗██║██║╚██╗██║ ╚██╔╝ ", CP_LOGO4, True),
252
+ (" ╚██████╗ ╚██████╔╝██║ ╚████║██║ ╚████║ ██║ ", CP_LOGO5, True),
253
+ (" ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚═╝ ", CP_LOGO6, True),
254
+ (" Multi-sector. Multi-canal. Siempre disponible.", CP_DIM, False),
255
+ (f" ✦ Conny v{VERSION} · 20 sectores", CP_TITLE, True),
256
+ (_SEP_LINE, CP_BAR, False),
257
+ ]
258
+
259
+ LOGO_COMPACT = " ✦ conny"
260
+
261
+
262
+ def _draw_logo(win, row: int, cols: int, compact: bool = False) -> int:
263
+ """Dibuja el logo. Devuelve la siguiente fila libre."""
264
+ if compact or cols < 64:
265
+ # Modo compacto: una sola línea
266
+ try:
267
+ win.addstr(row, 0, LOGO_COMPACT, curses.color_pair(CP_LOGO) | curses.A_BOLD)
268
+ win.addstr(row, len(LOGO_COMPACT) + 2,
269
+ f"v{VERSION}", curses.color_pair(CP_DIM))
270
+ except curses.error:
271
+ pass
272
+ return row + 2
273
+ else:
274
+ # Modo completo: idéntico a `conny init`
275
+ for i, (line, pair, bold) in enumerate(LOGO_LINES):
276
+ try:
277
+ attr = curses.color_pair(pair)
278
+ if bold:
279
+ attr |= curses.A_BOLD
280
+ win.addstr(row + i, 0, line[:cols - 1], attr)
281
+ except curses.error:
282
+ pass
283
+ return row + len(LOGO_LINES) + 1
284
+
285
+
286
+ def _draw_instances(win, row: int, instances: List[Instance], cols: int) -> int:
287
+ """Dibuja la barra de estado de instancias."""
288
+ try:
289
+ win.addstr(row, 0, " ", curses.color_pair(CP_DIM))
290
+ win.addstr(row, 2, "Instancias: ", curses.color_pair(CP_DIM))
291
+ except curses.error:
292
+ pass
293
+
294
+ col = 14
295
+ clients = [i for i in instances if not i.is_base]
296
+ if not clients:
297
+ try:
298
+ win.addstr(row, col, "ninguna aún — usa Nueva instancia",
299
+ curses.color_pair(CP_WARN))
300
+ except curses.error:
301
+ pass
302
+ return row + 2
303
+
304
+ for inst in clients[:6]:
305
+ dot_attr = curses.color_pair(CP_OK) if inst.online else curses.color_pair(CP_WARN)
306
+ label_s = f" {inst.label[:18]} "
307
+ try:
308
+ win.addstr(row, col, inst.status_char, dot_attr | curses.A_BOLD)
309
+ win.addstr(row, col + 2, label_s[:cols - col - 4],
310
+ curses.color_pair(CP_TITLE))
311
+ except curses.error:
312
+ pass
313
+ col += len(label_s) + 3
314
+ if col > cols - 20:
315
+ break
316
+
317
+ if len(clients) > 6:
318
+ try:
319
+ win.addstr(row, col, f"+{len(clients)-6} más",
320
+ curses.color_pair(CP_DIM))
321
+ except curses.error:
322
+ pass
323
+ return row + 2
324
+
325
+
326
+ def _draw_menu(win, row: int, items: list, selected: int, cols: int) -> int:
327
+ """Dibuja el menú con highlight en item seleccionado."""
328
+ pad = 4
329
+ width = min(cols - pad * 2, 68)
330
+
331
+ try:
332
+ win.addstr(row, pad, "┌" + "─" * (width - 2) + "┐",
333
+ curses.color_pair(CP_BAR))
334
+ except curses.error:
335
+ pass
336
+ row += 1
337
+
338
+ for idx, (action_id, emoji, label, desc, _req) in enumerate(items):
339
+ is_sel = (idx == selected)
340
+
341
+ prefix = " ▶ " if is_sel else " "
342
+ line = f"{prefix}{emoji} {label}"
343
+ if desc and not is_sel:
344
+ gap = width - 4 - len(line)
345
+ line = line + " " * max(0, gap - len(desc) - 2) + f" {desc}"
346
+
347
+ inner = line[:width - 4]
348
+ inner = inner + " " * max(0, width - 4 - len(inner))
349
+
350
+ try:
351
+ if is_sel:
352
+ win.addstr(row, pad, " │", curses.color_pair(CP_BAR))
353
+ win.addstr(row, pad + 3,
354
+ f" {inner} ",
355
+ curses.color_pair(CP_SEL) | curses.A_BOLD)
356
+ win.addstr(row, pad + 3 + 2 + len(inner) + 2,
357
+ "│", curses.color_pair(CP_BAR))
358
+ else:
359
+ win.addstr(row, pad, " │", curses.color_pair(CP_BAR))
360
+ main_part = f" {prefix}{emoji} {label}"
361
+ win.addstr(row, pad + 3,
362
+ main_part[:width - 4],
363
+ curses.color_pair(CP_TITLE))
364
+ if desc:
365
+ dp = pad + 3 + len(main_part)
366
+ gap = width - 4 - len(main_part) - len(desc) - 2
367
+ if gap > 0 and dp + gap + len(desc) + 4 < cols:
368
+ win.addstr(row, dp + max(1, gap), desc[:24],
369
+ curses.color_pair(CP_DIM))
370
+ avail_end = pad + 3 + width - 4
371
+ win.addstr(row, avail_end, " │", curses.color_pair(CP_BAR))
372
+ except curses.error:
373
+ pass
374
+
375
+ row += 1
376
+
377
+ try:
378
+ win.addstr(row, pad, "└" + "─" * (width - 2) + "┘",
379
+ curses.color_pair(CP_BAR))
380
+ except curses.error:
381
+ pass
382
+ row += 2
383
+
384
+ hint = " ↑ ↓ navegar Enter seleccionar q / Esc salir"
385
+ try:
386
+ win.addstr(row, 0, hint[:cols - 1], curses.color_pair(CP_HINT))
387
+ except curses.error:
388
+ pass
389
+ return row + 1
390
+
391
+
392
+ # ── Main TUI loop ────────────────────────────────────────────────────────────
393
+
394
+ def _tui_main(stdscr) -> None:
395
+ curses.curs_set(0)
396
+ stdscr.keypad(True)
397
+ curses.noecho()
398
+ curses.cbreak()
399
+ stdscr.nodelay(False)
400
+
401
+ curses.start_color()
402
+ curses.use_default_colors()
403
+
404
+ # Paleta — gradiente CONNY (rosa → lavanda)
405
+ curses.init_pair(CP_LOGO1, 213, -1) # Rosa brillante
406
+ curses.init_pair(CP_LOGO2, 218, -1) # Rosa claro
407
+ curses.init_pair(CP_LOGO3, 183, -1) # Lila
408
+ curses.init_pair(CP_LOGO4, 177, -1) # Púrpura
409
+ curses.init_pair(CP_LOGO5, 147, -1) # Púrpura-azul
410
+ curses.init_pair(CP_LOGO6, 141, -1) # Lavanda
411
+ curses.init_pair(CP_TITLE, 15, -1) # Blanco
412
+ curses.init_pair(CP_DIM, 240, -1) # Gris dim
413
+ curses.init_pair(CP_SEL, 15, 57) # Blanco sobre púrpura (highlight)
414
+ curses.init_pair(CP_OK, 114, -1) # Verde ✓
415
+ curses.init_pair(CP_WARN, 221, -1) # Amarillo ⚠
416
+ curses.init_pair(CP_BAR, 241, -1) # Bordes (gris medio)
417
+ curses.init_pair(CP_INST, 141, -1) # Instancia activa
418
+ curses.init_pair(CP_HINT, 236, -1) # Hint muy dim
419
+
420
+ selected = 0
421
+ instances = []
422
+ last_load = 0.0
423
+ need_reload = True
424
+
425
+ while True:
426
+ now = time.monotonic()
427
+ if need_reload or now - last_load > 10:
428
+ instances = _load_instances()
429
+ last_load = now
430
+ need_reload = False
431
+
432
+ rows, cols = stdscr.getmaxyx()
433
+ stdscr.erase()
434
+
435
+ compact = rows < 22 # Menos filas → modo compacto
436
+
437
+ r = 0
438
+ r = _draw_logo(stdscr, r, cols, compact=compact)
439
+ r = _draw_instances(stdscr, r, instances, cols)
440
+ r = _draw_menu(stdscr, r, MENU_ITEMS, selected, cols)
441
+
442
+ stdscr.refresh()
443
+
444
+ try:
445
+ key = stdscr.getch()
446
+ except KeyboardInterrupt:
447
+ break
448
+
449
+ if key in (ord('q'), ord('Q'), 27): # q / Esc
450
+ break
451
+ elif key == curses.KEY_UP:
452
+ selected = (selected - 1) % len(MENU_ITEMS)
453
+ elif key == curses.KEY_DOWN:
454
+ selected = (selected + 1) % len(MENU_ITEMS)
455
+ elif key == curses.KEY_HOME:
456
+ selected = 0
457
+ elif key == curses.KEY_END:
458
+ selected = len(MENU_ITEMS) - 1
459
+ elif key in (curses.KEY_ENTER, 10, 13):
460
+ action_id = MENU_ITEMS[selected][0]
461
+ if action_id == "_exit":
462
+ break
463
+ _execute_menu_action(action_id, instances)
464
+ need_reload = True
465
+ elif key == curses.KEY_RESIZE:
466
+ stdscr.erase()
467
+ stdscr.refresh()
468
+
469
+
470
+ # ── Entry point ──────────────────────────────────────────────────────────────
471
+
472
+ def run_tui() -> None:
473
+ """
474
+ Lanza el TUI interactivo. Llamar desde conny_cli.py main()
475
+ cuando no se pasan argumentos en una terminal real.
476
+ """
477
+ if not sys.stdout.isatty():
478
+ _simple_help()
479
+ return
480
+
481
+ try:
482
+ curses.wrapper(_tui_main)
483
+ except curses.error as e:
484
+ print(f"\n (TUI no disponible en esta terminal: {e})")
485
+ print(" Usa: conny help\n")
486
+ except Exception as e:
487
+ curses.endwin()
488
+ print(f"\n Error en TUI: {e}")
489
+ print(" Usa: conny help\n")
490
+
491
+
492
+ def _simple_help() -> None:
493
+ """Fallback en terminales sin soporte interactivo."""
494
+ print(f"""
495
+ ✦ conny v{VERSION}
496
+
497
+ Comandos esenciales:
498
+ conny new Crear instancia para un cliente
499
+ conny sync Sincronizar updates a todas las instancias
500
+ conny list Ver todas las instancias
501
+ conny dashboard Panel en tiempo real
502
+ conny health Health check rápido
503
+ conny logs [n] Logs en vivo
504
+ conny restart [n] Reiniciar instancia
505
+ conny doctor Diagnóstico del sistema
506
+
507
+ Instancias en: {INSTANCES_DIR}
508
+ """)
509
+
510
+
511
+ if __name__ == "__main__":
512
+ run_tui()