@innvisor/conny-ai 9.7.0 → 9.8.2

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 (50) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +17 -1
  3. package/conny_app.py +9 -3
  4. package/conny_cli.py +103 -11
  5. package/conny_core/evolution.py +112 -0
  6. package/conny_core/first_turn_ops.py +16 -20
  7. package/conny_core/prompt_ops.py +62 -0
  8. package/conny_demo_voice.py +1 -1
  9. package/conny_doctor.py +287 -2
  10. package/conny_domino.py +2 -2
  11. package/conny_generator.py +1 -1
  12. package/conny_i18n.py +81 -2
  13. package/conny_init.py +254 -41
  14. package/conny_runtime_ops.py +198 -6
  15. package/conny_tui.py +7 -0
  16. package/conny_ultra_config.py +25 -11
  17. package/conny_utils.py +21 -3
  18. package/ecosystem.config.js +11 -1
  19. package/install.sh +78 -22
  20. package/npm/conny.js +75 -21
  21. package/package.json +12 -2
  22. package/run.sh +7 -0
  23. package/src/conny/admin/dashboard.py +35 -4
  24. package/src/conny/admin_memory.py +93 -0
  25. package/src/conny/api/routes.py +26 -9
  26. package/src/conny/channels/cli.py +30 -9
  27. package/src/conny/demo/handler.py +23 -23
  28. package/src/conny/personas/generator.py +1 -1
  29. package/src/conny/production/domino.py +2 -2
  30. package/src/conny/production/guard.py +4 -4
  31. package/src/core/admin_engines.py +51 -48
  32. package/src/core/globals.py +110 -9
  33. package/src/core/production_monitor.py +63 -38
  34. package/src/core/runtime.py +343 -305
  35. package/src/domain/prompts/prospect_pitch.py +11 -11
  36. package/src/domain/send_guard.py +4 -4
  37. package/src/interfaces/web/app.py +91 -27
  38. package/src/interfaces/web/demo_admin_commands.py +165 -0
  39. package/src/interfaces/web/demo_handler.py +178 -34
  40. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +0 -22
  41. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +0 -11
  42. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +0 -11
  43. package/brand-assets/cl-nica-las-am-ricas/manifest.json +0 -22
  44. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +0 -11
  45. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +0 -11
  46. package/brand-assets/conny-demo/manifest.json +0 -22
  47. package/brand-assets/conny-demo/processed/business-identity.txt +0 -7
  48. package/brand-assets/conny-demo/raw/business-identity.txt +0 -7
  49. package/fix_init.py +0 -27
  50. package/verify_conversation_impl.py +0 -48
@@ -6,18 +6,23 @@ import json
6
6
  import os
7
7
  import re
8
8
  import shutil
9
+ import select
10
+ import shlex
9
11
  import socket
10
12
  import subprocess
11
13
  import sys
14
+ import time
12
15
  from pathlib import Path
13
16
  from typing import Any, Dict, List, Optional
14
17
 
15
18
 
16
19
  CONNY_HOME = Path(os.getenv("CONNY_HOME", str(Path.home() / ".conny")))
17
20
  CONNY_DIR = Path(os.getenv("CONNY_DIR", str(Path(__file__).resolve().parent)))
18
- INSTANCES_DIR = Path(os.getenv("INSTANCES_DIR", str(Path.home() / "conny-instances")))
21
+ INSTANCES_DIR = Path(os.getenv("INSTANCES_DIR", str(CONNY_HOME / "instances")))
22
+ ACTIVE_INSTANCE_PATH = Path(os.getenv("CONNY_ACTIVE_INSTANCE", str(CONNY_HOME / "active_instance")))
19
23
 
20
24
  _TUNNEL_PORT_PATTERNS = (
25
+ re.compile(r"-R\s+\d+:(?:localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)", re.I),
21
26
  re.compile(r"localhost:(\d+)"),
22
27
  re.compile(r"127\.0\.0\.1:(\d+)"),
23
28
  re.compile(r"0\.0\.0\.0:(\d+)"),
@@ -26,6 +31,30 @@ _TUNNEL_PORT_PATTERNS = (
26
31
  re.compile(r"lt\s+--port\s+(\d+)", re.I),
27
32
  )
28
33
 
34
+ _PUBLIC_URL_PATTERN = re.compile(r"https://[a-zA-Z0-9.-]+(?:lhr\.life|localhost\.run)(?:/[^\s]*)?")
35
+
36
+
37
+ _ENV_KEY_PATTERN = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=")
38
+
39
+
40
+ def _clean_env_value(value: str) -> str:
41
+ value = str(value or "").strip()
42
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
43
+ value = value[1:-1].strip()
44
+ if value.lower() in {"pending", "none", "null"}:
45
+ return ""
46
+ return value
47
+
48
+
49
+ def _format_env_line(key: str, value: str) -> str:
50
+ value = _clean_env_value(value)
51
+ if not value:
52
+ return f"{key}="
53
+ if any(ch.isspace() for ch in value) or any(ch in value for ch in ['"', "'", "#"]):
54
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
55
+ return f'{key}="{escaped}"'
56
+ return f"{key}={value}"
57
+
29
58
 
30
59
  def load_env_file(path: Path) -> Dict[str, str]:
31
60
  data: Dict[str, str] = {}
@@ -36,7 +65,10 @@ def load_env_file(path: Path) -> Dict[str, str]:
36
65
  if not line or line.startswith("#") or "=" not in line:
37
66
  continue
38
67
  key, value = line.split("=", 1)
39
- data[key.strip()] = value.strip().strip('"').strip("'")
68
+ key = key.strip()
69
+ if key.startswith("export "):
70
+ key = key.split(None, 1)[1].strip()
71
+ data[key] = _clean_env_value(value)
40
72
  return data
41
73
 
42
74
 
@@ -47,22 +79,51 @@ def write_env_value(path: Path, key: str, value: str) -> None:
47
79
  lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
48
80
  updated: List[str] = []
49
81
  for raw_line in lines:
50
- if raw_line.strip().startswith(f"{key}="):
51
- updated.append(f"{key}={value}")
82
+ match = _ENV_KEY_PATTERN.match(raw_line)
83
+ if match and match.group(1) == key:
84
+ updated.append(_format_env_line(key, value))
52
85
  found = True
53
86
  else:
54
87
  updated.append(raw_line)
55
88
  if not found:
56
89
  if updated and updated[-1].strip():
57
90
  updated.append("")
58
- updated.append(f"{key}={value}")
91
+ updated.append(_format_env_line(key, value))
59
92
  path.parent.mkdir(parents=True, exist_ok=True)
60
93
  path.write_text("\n".join(updated).rstrip() + "\n", encoding="utf-8")
61
94
 
62
95
 
96
+ def active_instance_name() -> str:
97
+ explicit = os.getenv("CONNY_INSTANCE", "").strip()
98
+ if explicit:
99
+ return explicit
100
+ if ACTIVE_INSTANCE_PATH.exists():
101
+ return ACTIVE_INSTANCE_PATH.read_text(encoding="utf-8", errors="replace").strip()
102
+ return ""
103
+
104
+
105
+ def set_active_instance(instance_name: str) -> None:
106
+ instance_name = str(instance_name or "").strip()
107
+ if not instance_name:
108
+ return
109
+ ACTIVE_INSTANCE_PATH.parent.mkdir(parents=True, exist_ok=True)
110
+ ACTIVE_INSTANCE_PATH.write_text(instance_name + "\n", encoding="utf-8")
111
+
112
+
63
113
  def instance_root(instance_name: str) -> Path:
64
114
  normalized = (instance_name or "").strip()
65
- if normalized in {"", "base", "conny", "default"}:
115
+ if normalized == "base":
116
+ return CONNY_DIR
117
+ if normalized in {"", "conny", "default"}:
118
+ active = active_instance_name()
119
+ if active:
120
+ active_root = INSTANCES_DIR / active
121
+ if active_root.exists():
122
+ return active_root
123
+ if normalized in {"conny", "default"}:
124
+ candidate = INSTANCES_DIR / normalized
125
+ if candidate.exists():
126
+ return candidate
66
127
  return CONNY_DIR
67
128
  return INSTANCES_DIR / normalized
68
129
 
@@ -98,6 +159,64 @@ def instance_runtime_info(instance_name: str) -> Dict[str, Any]:
98
159
  }
99
160
 
100
161
 
162
+ MIRRORED_ENV_KEYS = {
163
+ "INSTANCE_ID",
164
+ "PORT",
165
+ "HOST",
166
+ "BASE_URL",
167
+ "PUBLIC_BASE_URL",
168
+ "DASHBOARD_URL",
169
+ "PUBLIC_DASHBOARD_URL",
170
+ "DEMO_MODE",
171
+ "PLATFORM",
172
+ "SECTOR",
173
+ "BUSINESS_NAME",
174
+ "WEBHOOK_SECRET",
175
+ "TUNNEL_PROVIDER",
176
+ "TUNNEL_PID",
177
+ "TUNNEL_COMMAND",
178
+ "TELEGRAM_TOKEN",
179
+ "LLM_PROVIDER",
180
+ "LLM_MODEL",
181
+ "LLM_REASONING",
182
+ "LLM_FAST",
183
+ "LLM_LITE",
184
+ "CUSTOM_API_BASE",
185
+ "OPENAI_API_KEY",
186
+ "ANTHROPIC_API_KEY",
187
+ "GEMINI_API_KEY",
188
+ "GEMINI_API_KEY_2",
189
+ "GEMINI_API_KEY_3",
190
+ "GEMINI_API_KEY_4",
191
+ "GEMINI_API_KEY_5",
192
+ "GEMINI_API_KEY_6",
193
+ "GEMINI_API_KEY_7",
194
+ "GROQ_API_KEY",
195
+ "OPENROUTER_API_KEY",
196
+ "BRAVE_API_KEY",
197
+ "APIFY_API_KEY",
198
+ "SERP_API_KEY",
199
+ "WA_CLOUD_TOKEN",
200
+ "WA_PHONE_NUMBER_ID",
201
+ "CONNY_PYTHON_BIN",
202
+ }
203
+
204
+
205
+ def mirror_instance_env_to_base(instance_name: str) -> None:
206
+ info = instance_runtime_info(instance_name)
207
+ if info["root"] == CONNY_DIR:
208
+ return
209
+ env = load_env_file(info["env_path"])
210
+ if not env:
211
+ return
212
+ base_env_path = CONNY_DIR / ".env"
213
+ for key in sorted(MIRRORED_ENV_KEYS):
214
+ if key in env:
215
+ write_env_value(base_env_path, key, env.get(key, ""))
216
+ write_env_value(base_env_path, "ACTIVE_INSTANCE", info["name"])
217
+ set_active_instance(info["name"])
218
+
219
+
101
220
  def port_is_open(port: int, host: str = "127.0.0.1", timeout: float = 0.5) -> bool:
102
221
  try:
103
222
  with socket.create_connection((host, int(port)), timeout=timeout):
@@ -186,6 +305,7 @@ def extract_tunnel_target_ports(command_line: str) -> List[int]:
186
305
  def rewrite_tunnel_command_port(command_line: str, new_port: int) -> str:
187
306
  updated = str(command_line or "")
188
307
  replacements = [
308
+ (re.compile(r"(-R\s+\d+:(?:localhost|127\.0\.0\.1|0\.0\.0\.0):)(\d+)", re.I), rf"\g<1>{int(new_port)}"),
189
309
  (re.compile(r"(localhost:)(\d+)"), rf"\g<1>{int(new_port)}"),
190
310
  (re.compile(r"(127\.0\.0\.1:)(\d+)"), rf"\g<1>{int(new_port)}"),
191
311
  (re.compile(r"(0\.0\.0\.0:)(\d+)"), rf"\g<1>{int(new_port)}"),
@@ -200,6 +320,78 @@ def rewrite_tunnel_command_port(command_line: str, new_port: int) -> str:
200
320
  return updated
201
321
 
202
322
 
323
+ def localhost_run_command(port: int) -> List[str]:
324
+ return [
325
+ "ssh",
326
+ "-o", "StrictHostKeyChecking=no",
327
+ "-o", "UserKnownHostsFile=/dev/null",
328
+ "-o", "ServerAliveInterval=60",
329
+ "-N",
330
+ "-R", f"80:localhost:{int(port)}",
331
+ "nokey@localhost.run",
332
+ ]
333
+
334
+
335
+ def start_localhost_run_tunnel(port: int, timeout: float = 25.0) -> Dict[str, Any]:
336
+ if not shutil.which("ssh"):
337
+ return {"ok": False, "url": "", "pid": None, "error": "ssh no está instalado"}
338
+ command = localhost_run_command(port)
339
+ try:
340
+ proc = subprocess.Popen(
341
+ command,
342
+ stdout=subprocess.PIPE,
343
+ stderr=subprocess.STDOUT,
344
+ text=True,
345
+ bufsize=1,
346
+ start_new_session=True,
347
+ )
348
+ except Exception as exc:
349
+ return {"ok": False, "url": "", "pid": None, "error": str(exc)}
350
+
351
+ deadline = time.time() + float(timeout)
352
+ output: List[str] = []
353
+ stream = proc.stdout
354
+ while time.time() < deadline:
355
+ if proc.poll() is not None:
356
+ break
357
+ if stream is None:
358
+ time.sleep(0.25)
359
+ continue
360
+ try:
361
+ ready, _, _ = select.select([stream], [], [], 0.5)
362
+ except Exception:
363
+ ready = []
364
+ if not ready:
365
+ continue
366
+ line = stream.readline()
367
+ if not line:
368
+ continue
369
+ output.append(line.rstrip())
370
+ match = _PUBLIC_URL_PATTERN.search(line)
371
+ if match:
372
+ return {
373
+ "ok": True,
374
+ "url": match.group(0).rstrip("/"),
375
+ "pid": proc.pid,
376
+ "command": shlex.join(command),
377
+ "port": int(port),
378
+ "output": "\n".join(output[-10:]),
379
+ }
380
+
381
+ try:
382
+ proc.terminate()
383
+ except Exception:
384
+ pass
385
+ return {
386
+ "ok": False,
387
+ "url": "",
388
+ "pid": proc.pid,
389
+ "command": shlex.join(command),
390
+ "port": int(port),
391
+ "error": "\n".join(output[-10:]) or f"no public URL received after {int(timeout)}s",
392
+ }
393
+
394
+
203
395
  def detect_tunnel_processes() -> List[Dict[str, Any]]:
204
396
  try:
205
397
  result = subprocess.run(
package/conny_tui.py CHANGED
@@ -30,6 +30,13 @@ CONNY_DIR = os.getenv("CONNY_DIR", str(Path(__file__).resolve().parent))
30
30
  INSTANCES_DIR = os.getenv("INSTANCES_DIR", str(Path.home() / "conny-instances"))
31
31
  CLI_SCRIPT = os.getenv("CONNY_CLI", str(Path(__file__).resolve().parent / "conny_cli.py"))
32
32
 
33
+ try:
34
+ _package_path = Path(CONNY_DIR) / "package.json"
35
+ if _package_path.exists():
36
+ VERSION = json.loads(_package_path.read_text(encoding="utf-8")).get("version", VERSION)
37
+ except Exception:
38
+ pass
39
+
33
40
  # ── Colores curses (índices de par) ─────────────────────────────────────────
34
41
  CP_LOGO1 = 1 # Rosa — letras CONNY (gradiente línea 1)
35
42
  CP_LOGO2 = 10 # Rosa claro (gradiente línea 2)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """CONNY ULTRA CONFIG v9.7.0 — interactive runtime control panel."""
2
+ """Interactive runtime control panel for Conny."""
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
@@ -15,6 +15,7 @@ from conny_runtime_ops import (
15
15
  find_pm2_processes,
16
16
  health_payload,
17
17
  instance_runtime_info,
18
+ mirror_instance_env_to_base,
18
19
  port_is_open,
19
20
  python_candidates,
20
21
  resolve_python,
@@ -70,14 +71,19 @@ def _load_state(instance_name: str) -> Dict[str, Any]:
70
71
  }
71
72
 
72
73
 
74
+ def _mirror_if_needed(info: Dict[str, Any]) -> None:
75
+ if info["name"] == "base":
76
+ return
77
+ try:
78
+ mirror_instance_env_to_base(info["name"])
79
+ except Exception:
80
+ pass
81
+
82
+
73
83
  def _render_header(instance_name: str, subtitle: str) -> None:
74
84
  clear_screen()
75
- print(f"{''*68}")
76
- print(f"{BOLD} CONNY ULTRA CONFIG v9.7.0 {RESET}")
77
- print(f"├{'─'*68}┤")
78
- print(f"│ {WHITE}{BOLD}Instancia:{RESET} {instance_name or 'base':<57}│")
79
- print(f"│ {CYAN}{subtitle:<66}{RESET}│")
80
- print(f"└{'─'*68}┘")
85
+ print(f"{MAGENTA}{BOLD}Conny Config{RESET} {DIM}· {instance_name or 'base'} · {subtitle}{RESET}")
86
+ print(f"{DIM}{'─' * 60}{RESET}")
81
87
  print()
82
88
 
83
89
 
@@ -162,7 +168,7 @@ def _test_provider(provider_key: str, secret: str) -> Tuple[bool, str]:
162
168
 
163
169
 
164
170
  def _sync_telegram_webhook(state: Dict[str, Any]) -> Tuple[bool, str]:
165
- info = state["info"]
171
+ info = instance_runtime_info(state["info"]["name"])
166
172
  token = info["telegram_token"]
167
173
  base_url = info["base_url"]
168
174
  secret = info["webhook_secret"]
@@ -170,6 +176,8 @@ def _sync_telegram_webhook(state: Dict[str, Any]) -> Tuple[bool, str]:
170
176
  return False, "faltan TELEGRAM_TOKEN, BASE_URL o WEBHOOK_SECRET"
171
177
  target_url = f"{base_url.rstrip('/')}/webhook/{secret}"
172
178
  try:
179
+ if info.get("pm2_name"):
180
+ subprocess.run(["pm2", "restart", info["pm2_name"], "--update-env"], capture_output=True, check=False, timeout=20)
173
181
  import httpx
174
182
  with httpx.Client(timeout=10.0) as client:
175
183
  response = client.post(
@@ -235,6 +243,7 @@ def module_network(instance_name: str) -> None:
235
243
  new_port = text_input("Nuevo puerto", default=str(port))
236
244
  if new_port.isdigit():
237
245
  write_env_value(info["env_path"], "PORT", new_port)
246
+ _mirror_if_needed(info)
238
247
  meta_path = Path(info["root"]) / "instance.json"
239
248
  if meta_path.exists():
240
249
  try:
@@ -278,6 +287,7 @@ def module_models(instance_name: str) -> None:
278
287
  current = env.get(key, "")
279
288
  new_value = text_input(f"{label} API key", default=current, is_password=bool(current), required=False)
280
289
  write_env_value(info["env_path"], key, new_value)
290
+ _mirror_if_needed(info)
281
291
  print(f"\n{GREEN}✓ {label} actualizada{RESET}")
282
292
  elif choice == 1:
283
293
  idx = select_menu([label for _, label in _provider_env_keys()], title="¿Cuál key quieres probar?")
@@ -291,6 +301,7 @@ def module_models(instance_name: str) -> None:
291
301
  current = env.get(selected, "")
292
302
  new_value = text_input(f"{selected}", default=current or "google/gemini-2.5-flash")
293
303
  write_env_value(info["env_path"], selected, new_value)
304
+ _mirror_if_needed(info)
294
305
  print(f"\n{GREEN}✓ {selected} actualizado{RESET}")
295
306
  wait_for_enter()
296
307
 
@@ -318,6 +329,8 @@ def module_gateway(instance_name: str) -> None:
318
329
  elif choice == 1:
319
330
  new_url = text_input("Nueva BASE_URL", default=info["base_url"], required=False)
320
331
  write_env_value(info["env_path"], "BASE_URL", new_url)
332
+ write_env_value(info["env_path"], "PUBLIC_BASE_URL", new_url)
333
+ _mirror_if_needed(info)
321
334
  print(f"\n{GREEN}✓ BASE_URL actualizada{RESET}")
322
335
  wait_for_enter()
323
336
 
@@ -345,6 +358,7 @@ def module_environment(instance_name: str) -> None:
345
358
  idx = select_menu([f"{c['source']} :: {c['path']}" for c in valid], title="Selecciona intérprete")
346
359
  selected = valid[idx]
347
360
  write_env_value(info["env_path"], "CONNY_PYTHON_BIN", selected["path"])
361
+ _mirror_if_needed(info)
348
362
  print(f"\n{GREEN}✓ CONNY_PYTHON_BIN fijado a {selected['path']}{RESET}")
349
363
  elif choice == 1:
350
364
  run_path = Path(info["root"]) / "run.sh"
@@ -358,7 +372,7 @@ def module_doctor(instance_name: str) -> None:
358
372
  print(f"{MAGENTA}Iniciando Self-Healing...{RESET}\n")
359
373
  try:
360
374
  import conny_doctor
361
- doctor = conny_doctor.ConnyDoctor(instance_name or "base")
375
+ doctor = conny_doctor.ConnyDoctor(instance_name or "conny")
362
376
  asyncio.run(doctor.run_self_healing())
363
377
  except Exception as exc:
364
378
  print(f"{MAGENTA}Error ejecutando doctor: {exc}{RESET}")
@@ -366,9 +380,9 @@ def module_doctor(instance_name: str) -> None:
366
380
 
367
381
 
368
382
  def run_ultra_config(instance_name: str = "") -> None:
369
- active = (instance_name or "base").strip()
383
+ active = (instance_name or "conny").strip()
370
384
  while True:
371
- _render_header(active, "CONTROL TOTAL DEL USUARIO")
385
+ _render_header(active, "Runtime overview")
372
386
  state = _load_state(active)
373
387
  port = state["info"]["port"]
374
388
  health = "online" if state["health"].get("status") == "online" else "offline"
package/conny_utils.py CHANGED
@@ -6,12 +6,21 @@ import re
6
6
  from typing import Any, Dict, List, Optional
7
7
 
8
8
  ACTIVATION_PREFIX = "ACTV-"
9
+ ADMIN_ACTIVATION_PREFIX = "ADMN-"
9
10
  INVITE_PREFIX = "JINV-"
10
11
 
11
12
  def is_activation_token(text: str) -> bool:
12
13
  """Detecta si el mensaje es un token de activacion."""
13
14
  t = text.strip().upper()
14
- return t.startswith(ACTIVATION_PREFIX) and len(t) >= 30
15
+ return (
16
+ (t.startswith(ACTIVATION_PREFIX) and len(t) >= 30)
17
+ or is_admin_activation_token(t)
18
+ )
19
+
20
+ def is_admin_activation_token(text: str) -> bool:
21
+ """Detecta tokens de Conny Pro Admin."""
22
+ t = text.strip().upper()
23
+ return t.startswith(ADMIN_ACTIVATION_PREFIX) and len(t) >= 30
15
24
 
16
25
  def is_invite_token(text: str) -> bool:
17
26
  """Detecta si el mensaje es un token de invitacion."""
@@ -24,12 +33,21 @@ def generate_activation_token(label: str) -> str:
24
33
  Formato: ACTV-[label_sanitizado]-[32_chars_hex]
25
34
  """
26
35
  import string
27
- sanitized = re.sub(r'[^a-zA-Z0-9]', '', label.lower())[:10]
36
+ sanitized = re.sub(r'[^a-zA-Z0-9]', '', label.lower())[:10].upper()
28
37
  if not sanitized:
29
- sanitized = "generic"
38
+ sanitized = "GENERIC"
30
39
  entropy = secrets.token_hex(16).upper()
31
40
  return f"{ACTIVATION_PREFIX}{sanitized}-{entropy}"
32
41
 
42
+ def generate_admin_activation_token(label: str) -> str:
43
+ """
44
+ Genera un token de activacion para Conny Pro Admin.
45
+ Este token activa una cuenta operadora, no una conversacion de cliente.
46
+ """
47
+ sanitized = (re.sub(r'[^a-zA-Z0-9]', '', label.lower())[:10] or "admin").upper()
48
+ entropy = secrets.token_hex(18).upper()
49
+ return f"{ADMIN_ACTIVATION_PREFIX}{sanitized}-{entropy}"
50
+
33
51
  def hash_password(password: str) -> str:
34
52
  """Hash de contrasena con PBKDF2 + salt."""
35
53
  salt = secrets.token_hex(16)
@@ -10,6 +10,16 @@ module.exports = {
10
10
  error_file: "/home/ubuntu/conny/logs/conny-error.log",
11
11
  watch: false,
12
12
  },
13
+ {
14
+ name: "whatsapp-bridge",
15
+ script: "/home/ubuntu/whatsapp-bridge/start.sh",
16
+ cwd: "/home/ubuntu/whatsapp-bridge",
17
+ restart_delay: 3000,
18
+ max_restarts: 10,
19
+ out_file: "/home/ubuntu/whatsapp-bridge/logs/bridge.log",
20
+ error_file: "/home/ubuntu/whatsapp-bridge/logs/bridge-error.log",
21
+ watch: false,
22
+ },
13
23
  {
14
24
  name: "conny-clinica-de-las-americas",
15
25
  script: "/home/ubuntu/conny-instances/clinica-de-las-americas/run.sh",
@@ -21,4 +31,4 @@ module.exports = {
21
31
  watch: false,
22
32
  }
23
33
  ]
24
- }
34
+ };
package/install.sh CHANGED
@@ -6,11 +6,54 @@ set -e
6
6
  C_PRIMARY="\033[38;5;135m"
7
7
  C_SUCCESS="\033[38;5;46m"
8
8
  C_MUTED="\033[38;5;240m"
9
+ C_WARN="\033[38;5;214m"
9
10
  BOLD="\033[1m"
10
11
  RESET="\033[0m"
11
12
 
12
- echo -e "\n ${C_PRIMARY}${BOLD}✦ Conny AI - Ultimate Installer${RESET}"
13
- echo -e " ${C_MUTED}─────────────────────────────────────────${RESET}"
13
+ echo -e "\n ${C_PRIMARY}${BOLD}✦ Conny AI Installer${RESET}"
14
+ echo -e " ${C_MUTED}Production-ready AI receptionist runtime for WhatsApp and Telegram.${RESET}"
15
+ echo -e " ${C_MUTED}────────────────────────────────────────────────────────${RESET}"
16
+
17
+ TMP_LOGS=()
18
+
19
+ cleanup_logs() {
20
+ for log_file in "${TMP_LOGS[@]}"; do
21
+ [ -f "$log_file" ] && rm -f "$log_file"
22
+ done
23
+ }
24
+ trap cleanup_logs EXIT
25
+
26
+ run_with_activity() {
27
+ local label="$1"
28
+ shift
29
+ local log_file
30
+ log_file="$(mktemp)"
31
+ TMP_LOGS+=("$log_file")
32
+
33
+ echo -ne " ${C_PRIMARY}⠋${RESET} ${label}"
34
+ "$@" >"$log_file" 2>&1 &
35
+ local pid=$!
36
+ local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
37
+ local i=0
38
+ local elapsed=0
39
+
40
+ while kill -0 "$pid" 2>/dev/null; do
41
+ printf "\r ${C_PRIMARY}%s${RESET} %s ${C_MUTED}(%ss)${RESET}" "${frames[$((i % ${#frames[@]}))]}" "$label" "$elapsed"
42
+ sleep 1
43
+ i=$((i + 1))
44
+ elapsed=$((elapsed + 1))
45
+ done
46
+
47
+ if wait "$pid"; then
48
+ printf "\r ${C_SUCCESS}✓${RESET} %s ${C_MUTED}(%ss)${RESET}\n" "$label" "$elapsed"
49
+ return 0
50
+ fi
51
+
52
+ printf "\r \033[31m✕${RESET} %s\n" "$label"
53
+ echo -e " \033[31mCommand failed. Installer log:${RESET}"
54
+ sed 's/^/ /' "$log_file"
55
+ return 1
56
+ }
14
57
 
15
58
  # Handle sudo gracefully (Termux / Root environments)
16
59
  SUDO=""
@@ -18,20 +61,22 @@ if command -v sudo &> /dev/null; then
18
61
  SUDO="sudo"
19
62
  fi
20
63
 
21
- # 1. Install chafa if possible
64
+ # 1. Install chafa if possible. In Termux/proot, `pkg` exists but cannot run as root.
22
65
  if ! command -v chafa &> /dev/null; then
23
- echo -e "\n ${BOLD}1. Instalando motor True-Color (chafa)...${RESET}"
24
- if command -v pkg &> /dev/null; then
25
- pkg install -y chafa || true
66
+ echo -e "\n ${BOLD}1. Preparing terminal visuals${RESET}"
67
+ CURRENT_UID="$(id -u 2>/dev/null || echo 1)"
68
+ if command -v pkg &> /dev/null && [ "$CURRENT_UID" != "0" ]; then
69
+ run_with_activity "Installing optional True-Color renderer with pkg" pkg install -y chafa || true
26
70
  elif command -v apt-get &> /dev/null; then
27
- $SUDO apt-get update -yqq && $SUDO apt-get install -yqq chafa || true
71
+ run_with_activity "Installing optional True-Color renderer with apt" bash -c "$SUDO apt-get update -yqq && $SUDO apt-get install -yqq chafa" || true
28
72
  elif command -v brew &> /dev/null; then
29
- brew install chafa || true
73
+ run_with_activity "Installing optional True-Color renderer with Homebrew" brew install chafa || true
30
74
  else
31
- echo -e " ${C_MUTED}No se pudo instalar chafa automáticamente. Se usará el logo clásico.${RESET}"
75
+ echo -e " ${C_WARN}!${RESET} Optional renderer not available. Conny will use the classic logo."
32
76
  fi
33
77
  else
34
- echo -e "\n ${BOLD}1. Motor True-Color detectado (chafa).${RESET}"
78
+ echo -e "\n ${BOLD}1. Terminal visuals ready${RESET}"
79
+ echo -e " ${C_SUCCESS}✓${RESET} True-Color renderer detected."
35
80
  fi
36
81
 
37
82
  # 2. Verify Python sanely (3.9+), without hardcoding minor versions
@@ -51,28 +96,39 @@ done
51
96
 
52
97
  if [ -n "$PYTHON_BIN" ]; then
53
98
  PY_VERSION="$($PYTHON_BIN -c 'import sys; print(".".join(map(str, sys.version_info[:3])))')"
54
- echo -e "\n ${BOLD}2. Python detectado:${RESET} ${C_SUCCESS}${PYTHON_BIN} (${PY_VERSION})${RESET}"
99
+ echo -e "\n ${BOLD}2. Runtime compatibility${RESET}"
100
+ echo -e " ${C_SUCCESS}✓${RESET} Python runtime detected: ${C_SUCCESS}${PYTHON_BIN} ${PY_VERSION}${RESET}"
55
101
  else
56
- echo -e "\n ${BOLD}2. Python 3.9+ no detectado localmente.${RESET}"
57
- echo -e " ${C_MUTED}Conny intentará crear su runtime cuando se ejecute por primera vez.${RESET}"
102
+ echo -e "\n ${BOLD}2. Runtime compatibility${RESET}"
103
+ echo -e " ${C_WARN}!${RESET} Python 3.9+ was not detected locally."
104
+ echo -e " ${C_MUTED}Conny will try to provision its isolated runtime on first launch.${RESET}"
58
105
  fi
59
106
 
60
107
  # 3. Install NPM Package
61
108
  if ! command -v npm &> /dev/null; then
62
- echo -e "\n \033[31mError: Node.js y npm son requeridos. Instálalos primero.\033[0m"
109
+ echo -e "\n \033[31mError: Node.js and npm are required before installing Conny.\033[0m"
63
110
  exit 1
64
111
  fi
65
112
 
66
- echo -e "\n ${BOLD}3. Limpiando versiones anteriores...${RESET}"
67
- npm uninstall -g conny-ai @blackboss/conny || true
113
+ echo -e "\n ${BOLD}3. Removing previous global builds${RESET}"
114
+ run_with_activity "Cleaning old Conny packages" npm uninstall -g conny-ai @innvisor/conny-ai @blackboss/conny || true
68
115
 
69
- echo -e "\n ${BOLD}4. Instalando Conny CLI y Motor AI...${RESET}"
70
- npm install -g "${CONNY_INSTALL_PACKAGE:-conny-ai@latest}"
116
+ echo -e "\n ${BOLD}4. Installing Conny from GitHub${RESET}"
117
+ echo -e " ${C_MUTED}Source: ${CONNY_INSTALL_PACKAGE:-github:sxrubyo/conny#main}${RESET}"
118
+ run_with_activity "Downloading and linking Conny CLI" npm install -g "${CONNY_INSTALL_PACKAGE:-github:sxrubyo/conny#main}"
71
119
 
72
- echo -e "\n ${BOLD}5. Verificando bootstrap del CLI...${RESET}"
120
+ echo -e "\n ${BOLD}5. Verifying the command line experience${RESET}"
73
121
  if command -v conny >/dev/null 2>&1; then
74
- conny --version || true
122
+ if ! conny --version; then
123
+ echo -e "\n \033[31mError: Conny was installed, but the CLI could not start.\033[0m"
124
+ exit 1
125
+ fi
126
+ run_with_activity "Preparing Python runtime and required CLI packages" conny --bootstrap-check
127
+ else
128
+ echo -e "\n \033[31mError: npm finished, but the 'conny' command is not available in PATH.\033[0m"
129
+ exit 1
75
130
  fi
76
131
 
77
- echo -e "\n ${C_SUCCESS}${BOLD} ¡Conny instalado con éxito!${RESET}"
78
- echo -e " Ejecuta ${C_PRIMARY}conny init${RESET} en tu terminal para empezar la magia.\n"
132
+ echo -e "\n ${C_SUCCESS}${BOLD} Conny is installed and ready.${RESET}"
133
+ echo -e " Start your first guided setup with ${C_PRIMARY}conny init${RESET}."
134
+ echo -e " For system repair and diagnostics, run ${C_PRIMARY}conny doctor --fix${RESET}.\n"