@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.
- package/.env.example +68 -0
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
- package/brand-assets/Conny.web.logo.png +0 -0
- package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/conny-demo/manifest.json +22 -0
- package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
- package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
- package/brand-assets/conny-logo.png +0 -0
- package/brand-assets/web.background.png +0 -0
- package/brand_assets.py +323 -0
- package/conny +28 -0
- package/conny-chat.py +579 -0
- package/conny-omni.py +3843 -0
- package/conny.py +113 -0
- package/conny_agents/__init__.py +1 -0
- package/conny_agents/agenda.py +1 -0
- package/conny_agents/captacion.py +1 -0
- package/conny_agents/conocimiento.py +1 -0
- package/conny_agents/escalacion.py +1 -0
- package/conny_agents/objeciones.py +1 -0
- package/conny_agents/seguimiento.py +1 -0
- package/conny_app.py +287 -0
- package/conny_audio.py +350 -0
- package/conny_audio_learn.py +84 -0
- package/conny_brain_v10.py +804 -0
- package/conny_bridge.py +656 -0
- package/conny_calendar.py +169 -0
- package/conny_cli.py +11784 -0
- package/conny_cli_bb.py +437 -0
- package/conny_commands.py +243 -0
- package/conny_config.py +215 -0
- package/conny_core/__init__.py +3 -0
- package/conny_core/conversation_engine.py +446 -0
- package/conny_core/first_turn_ops.py +287 -0
- package/conny_core/persona_registry.py +157 -0
- package/conny_core/prompt_ops.py +561 -0
- package/conny_cron.py +72 -0
- package/conny_demo_v2.py +209 -0
- package/conny_demo_voice.py +134 -0
- package/conny_design.py +43 -0
- package/conny_doctor.py +319 -0
- package/conny_domino.py +696 -0
- package/conny_generator.py +447 -0
- package/conny_google_auth.py +159 -0
- package/conny_i18n.py +619 -0
- package/conny_init.py +509 -0
- package/conny_integrations/__init__.py +4 -0
- package/conny_integrations/llm.py +1 -0
- package/conny_integrations/vault.py +77 -0
- package/conny_integrations/whatsapp.py +1 -0
- package/conny_intelligence.py +65 -0
- package/conny_learning.py +154 -0
- package/conny_memory.py +243 -0
- package/conny_memory_engine.py +292 -0
- package/conny_nova_proxy.py +170 -0
- package/conny_nuke_robot_phrases.py +493 -0
- package/conny_pairing.py +253 -0
- package/conny_patch.py +291 -0
- package/conny_persona_cli.py +150 -0
- package/conny_router.py +308 -0
- package/conny_runtime_ops.py +271 -0
- package/conny_session.py +516 -0
- package/conny_skills/__init__.py +1 -0
- package/conny_skills/demo_mode.py +35 -0
- package/conny_skills/text_processing.py +1 -0
- package/conny_skills/tone_detection.py +1 -0
- package/conny_smart_features.py +333 -0
- package/conny_studio.py +161 -0
- package/conny_sync_fix.py +306 -0
- package/conny_tui.py +512 -0
- package/conny_tui_select.py +202 -0
- package/conny_ultra_config.py +411 -0
- package/conny_uncertainty.py +174 -0
- package/conny_utils.py +87 -0
- package/conny_voice.py +156 -0
- package/conny_voice_engine.py +124 -0
- package/conny_web_search.py +66 -0
- package/conny_weekly_report.py +85 -0
- package/conny_worm.py +88 -0
- package/core/__init__.py +25 -0
- package/ecosystem.config.js +24 -0
- package/fix_init.py +27 -0
- package/install.sh +78 -0
- package/knowledge_base.py +330 -0
- package/nova/rules/default.yaml +37 -0
- package/nova_bridge.py +509 -0
- package/npm/conny.js +471 -0
- package/package.json +102 -0
- package/personas/conny/base/default.yaml +35 -0
- package/personas/conny/base/estetica_whatsapp.yaml +36 -0
- package/requirements.txt +14 -0
- package/run.sh +47 -0
- package/search.py +465 -0
- package/smart_handoff.py +1150 -0
- package/src/__init__.py +0 -0
- package/src/conny/__init__.py +0 -0
- package/src/conny/admin/__init__.py +0 -0
- package/src/conny/admin/api.py +234 -0
- package/src/conny/admin/dashboard.py +772 -0
- package/src/conny/api/__init__.py +0 -0
- package/src/conny/api/routes.py +8851 -0
- package/src/conny/brain/__init__.py +15 -0
- package/src/conny/brain/engine.py +804 -0
- package/src/conny/brain/learning.py +154 -0
- package/src/conny/brain/memory.py +324 -0
- package/src/conny/brain/smart_features.py +333 -0
- package/src/conny/brain/uncertainty.py +167 -0
- package/src/conny/channels/__init__.py +0 -0
- package/src/conny/channels/audio.py +316 -0
- package/src/conny/channels/cli.py +11795 -0
- package/src/conny/channels/logo_art.py +11 -0
- package/src/conny/channels/voice.py +156 -0
- package/src/conny/core/__init__.py +0 -0
- package/src/conny/core/config.py +215 -0
- package/src/conny/core/cron.py +72 -0
- package/src/conny/core/messenger.py +563 -0
- package/src/conny/core/router.py +297 -0
- package/src/conny/core/session.py +312 -0
- package/src/conny/demo/__init__.py +0 -0
- package/src/conny/demo/handler.py +3110 -0
- package/src/conny/integrations/__init__.py +19 -0
- package/src/conny/integrations/calendar.py +169 -0
- package/src/conny/integrations/knowledge.py +312 -0
- package/src/conny/integrations/search.py +66 -0
- package/src/conny/personas/__init__.py +0 -0
- package/src/conny/personas/generator.py +447 -0
- package/src/conny/production/__init__.py +0 -0
- package/src/conny/production/domino.py +696 -0
- package/src/conny/production/guard.py +550 -0
- package/src/conny/production/handoff.py +1150 -0
- package/src/conny/production/monitor.py +353 -0
- package/src/conny/utils/__init__.py +2 -0
- package/src/conny/utils/helpers.py +75 -0
- package/src/conny/utils/i18n.py +619 -0
- package/src/core/admin_engines.py +772 -0
- package/src/core/globals.py +11845 -0
- package/src/core/orchestrator.py +273 -0
- package/src/core/production_monitor.py +353 -0
- package/src/core/runtime.py +5487 -0
- package/src/domain/onboarding_flow.py +230 -0
- package/src/domain/prompts/__init__.py +1 -0
- package/src/domain/prompts/prospect_pitch.py +282 -0
- package/src/domain/send_guard.py +636 -0
- package/src/domain/swarm/queen.py +96 -0
- package/src/infrastructure/llm_providers/engine.py +487 -0
- package/src/interfaces/mcp_server.py +73 -0
- package/src/interfaces/nova_bridge.py +58 -0
- package/src/interfaces/web/admin_api.py +1379 -0
- package/src/interfaces/web/app.py +9408 -0
- package/src/interfaces/web/demo_handler.py +3450 -0
- package/src/interfaces/web/static/generate_avatars.py +46 -0
- package/v7/__init__.py +46 -0
- package/v7/agents/__init__.py +46 -0
- package/v7/agents/agenda.py +77 -0
- package/v7/agents/base.py +216 -0
- package/v7/agents/captacion.py +60 -0
- package/v7/agents/conocimiento.py +69 -0
- package/v7/agents/escalacion.py +83 -0
- package/v7/agents/objeciones.py +109 -0
- package/v7/agents/seguimiento.py +71 -0
- package/v7/memory/__init__.py +46 -0
- package/v7/memory/patient_profile.py +200 -0
- package/v7/orchestrator.py +275 -0
- package/v7/postprocess.py +127 -0
- package/v7/router.py +239 -0
- 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()
|