@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
package/conny_init.py CHANGED
@@ -12,13 +12,75 @@ import shutil
12
12
  import urllib.request
13
13
  import urllib.error
14
14
  import subprocess
15
+ import socket
15
16
  from deep_translator import GoogleTranslator
16
17
 
17
- CURRENT_LANG = 'es'
18
+ CURRENT_LANG = os.environ.get("CONNY_INIT_LANG", "en").lower()
19
+
20
+ TRANSLATIONS = {
21
+ "en": {
22
+ "Selección de idioma": "Language selection",
23
+ "Idioma preferido para Conny": "Preferred language for Conny",
24
+ "¿Comenzar configuración?": "Start guided setup?",
25
+ "Operación cancelada.": "Setup cancelled.",
26
+ "Tiempo estimado: ~3 min": "Estimated time: ~3 min",
27
+ "Identidad": "Identity",
28
+ "Nombre del negocio": "Business name",
29
+ "Dominio e Industria": "Domain and industry",
30
+ "Sector Principal": "Primary sector",
31
+ "Canales de Conectividad": "Connectivity channels",
32
+ "Canal Principal de Acceso": "Primary access channel",
33
+ "Gateway público": "Public gateway",
34
+ "¿Cómo deseas configurar el enlace público (BASE_URL)?": "How do you want to configure the public link (BASE_URL)?",
35
+ "Generar túnel automático (localhost.run)": "Generate automatic tunnel (localhost.run)",
36
+ "Configurar enlace manualmente (ingresar URL personalizada)": "Configure manually (enter a custom URL)",
37
+ "Introduce la URL pública de tu webhook": "Enter your public webhook URL",
38
+ "Levantando túnel seguro hacia localhost.run...": "Starting secure tunnel through localhost.run...",
39
+ "Túnel activo:": "Tunnel active:",
40
+ "No pude obtener un túnel automático. Ingresa una URL manual.": "I could not get an automatic tunnel. Enter a manual URL.",
41
+ "Dashboard web": "Web dashboard",
42
+ "¿Cómo quieres exponer la página web de Conny?": "How do you want to expose Conny's web dashboard?",
43
+ "Solo local (localhost)": "Local only (localhost)",
44
+ "Red/IP externa de este dispositivo": "Network/external IP for this device",
45
+ "URL pública personalizada": "Custom public URL",
46
+ "No configurar dashboard ahora": "Do not configure dashboard now",
47
+ "Introduce la URL pública del dashboard": "Enter the public dashboard URL",
48
+ "Motor de Inteligencia": "Intelligence engine",
49
+ "Tipo de proveedor": "Provider type",
50
+ "Proveedor Cloud": "Cloud provider",
51
+ "Modelo Específico": "Specific model",
52
+ "Modelo de Ollama": "Ollama model",
53
+ "Modelo NIM": "NIM model",
54
+ "Humanización y Tono": "Humanization and tone",
55
+ "Perfil de Voz": "Voice profile",
56
+ "Infraestructura y Secretos": "Infrastructure and secrets",
57
+ "Verificación Final": "Final verification",
58
+ "¿Procesar e Implementar Infraestructura?": "Provision and deploy infrastructure?",
59
+ "Cancelado.": "Cancelled.",
60
+ "Creando recursos...": "Creating resources...",
61
+ "Resumen de Configuración": "Configuration summary",
62
+ "Negocio:": "Business:",
63
+ "Sector:": "Sector:",
64
+ "Canal:": "Channel:",
65
+ "Proveedor:": "Provider:",
66
+ "Modelo:": "Model:",
67
+ "Voz:": "Voice:",
68
+ "Secretos:": "Secrets:",
69
+ "Directorio:": "Directory:",
70
+ "Control:": "Control:",
71
+ "Check:": "Check:",
72
+ "Lanzar:": "Launch:",
73
+ "Infraestructura Desplegada Exitosamente": "Infrastructure deployed successfully",
74
+ "Validando API key...": "Validating API key...",
75
+ "✅ API key validada correctamente.": "✅ API key validated successfully.",
76
+ }
77
+ }
18
78
 
19
79
  def _t(text):
20
80
  if CURRENT_LANG == 'es' or not text:
21
81
  return text
82
+ if CURRENT_LANG in TRANSLATIONS and text in TRANSLATIONS[CURRENT_LANG]:
83
+ return TRANSLATIONS[CURRENT_LANG][text]
22
84
  try:
23
85
  # Limpiar texto de colores si es necesario, o traducir directo
24
86
  return GoogleTranslator(source='es', target=CURRENT_LANG).translate(text)
@@ -30,6 +92,12 @@ from datetime import datetime
30
92
  sys.path.insert(0, str(Path(__file__).parent))
31
93
 
32
94
  from conny_tui_select import select_menu as _orig_select_menu, confirm as _orig_confirm, text_input as _orig_text_input
95
+ try:
96
+ from conny_runtime_ops import mirror_instance_env_to_base, set_active_instance, start_localhost_run_tunnel
97
+ except Exception:
98
+ mirror_instance_env_to_base = None
99
+ set_active_instance = None
100
+ start_localhost_run_tunnel = None
33
101
 
34
102
  def select_menu(options, title="", **kwargs):
35
103
  if title: title = _t(title)
@@ -61,7 +129,8 @@ C_ACCENT = "\033[38;5;159m" # Cyan
61
129
  B1 = "\033[1m"
62
130
  R = "\033[0m"
63
131
 
64
- INSTANCES_DIR = Path(os.environ.get("INSTANCES_DIR", str(Path.home() / ".conny-instances")))
132
+ CONNY_HOME = Path(os.environ.get("CONNY_HOME", str(Path.home() / ".conny")))
133
+ INSTANCES_DIR = Path(os.environ.get("INSTANCES_DIR", str(CONNY_HOME / "instances")))
65
134
  CONNY_DIR = Path(os.environ.get("CONNY_DIR", os.path.dirname(os.path.abspath(__file__))))
66
135
 
67
136
  SECTORS = [
@@ -93,8 +162,8 @@ TONES = [
93
162
  ]
94
163
 
95
164
  LANGUAGES = [
96
- ("es", "Español"),
97
165
  ("en", "English"),
166
+ ("es", "Español"),
98
167
  ("pt", "Português"),
99
168
  ]
100
169
 
@@ -138,13 +207,124 @@ def check_gpu():
138
207
 
139
208
  def validate_api_key(provider, key):
140
209
  # Dummy validation with spinner
141
- sys.stdout.write(f" {C_MUTED}Validando API key...{R}")
210
+ sys.stdout.write(f" {C_MUTED}{_t('Validando API key...')}{R}")
142
211
  sys.stdout.flush()
143
212
  time.sleep(1.5)
144
- sys.stdout.write(f"\r {C_SUCCESS}✅ API key validada correctamente.{R} \n")
213
+ sys.stdout.write(f"\r {C_SUCCESS}{_t('✅ API key validada correctamente.')}{R} \n")
145
214
  return True
146
215
 
147
216
 
217
+ def _persist_language(lang: str) -> None:
218
+ try:
219
+ workspace_config_path = Path(os.environ.get("CONNY_WORKSPACE_CONFIG", str(CONNY_HOME / "config.json")))
220
+ workspace_config_path.parent.mkdir(parents=True, exist_ok=True)
221
+ payload = {}
222
+ if workspace_config_path.exists():
223
+ try:
224
+ payload = json.loads(workspace_config_path.read_text(encoding="utf-8"))
225
+ if not isinstance(payload, dict):
226
+ payload = {}
227
+ except Exception:
228
+ payload = {}
229
+ payload["language"] = lang
230
+ payload["ui_language"] = lang
231
+ workspace_config_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
232
+ except Exception:
233
+ pass
234
+
235
+
236
+ def _next_available_port() -> int:
237
+ existing_ports = []
238
+ if INSTANCES_DIR.exists():
239
+ for d in INSTANCES_DIR.iterdir():
240
+ env = d / ".env"
241
+ if not env.exists():
242
+ continue
243
+ for line in env.read_text(encoding="utf-8", errors="replace").splitlines():
244
+ if line.startswith("PORT="):
245
+ try:
246
+ existing_ports.append(int(line.split("=", 1)[1].strip()))
247
+ except Exception:
248
+ pass
249
+ return max(existing_ports + [8003]) + 1
250
+
251
+
252
+ def _configure_gateway(port: int) -> dict:
253
+ options = [
254
+ "Generar túnel automático (localhost.run)",
255
+ "Configurar enlace manualmente (ingresar URL personalizada)",
256
+ ]
257
+ choice = select_menu(options, title="¿Cómo deseas configurar el enlace público (BASE_URL)?")
258
+ if choice == 0 and start_localhost_run_tunnel:
259
+ print(f"\n {C_MUTED}{_t('Levantando túnel seguro hacia localhost.run...')}{R}")
260
+ result = start_localhost_run_tunnel(port)
261
+ if result.get("ok") and result.get("url"):
262
+ print(f" {C_SUCCESS}✓ {_t('Túnel activo:')} {result['url']}{R}")
263
+ return {
264
+ "mode": "localhost.run",
265
+ "base_url": str(result["url"]).rstrip("/"),
266
+ "tunnel_pid": str(result.get("pid") or ""),
267
+ "tunnel_command": str(result.get("command") or ""),
268
+ }
269
+ print(f" {C_WARNING}⚠ {_t('No pude obtener un túnel automático. Ingresa una URL manual.')}{R}")
270
+
271
+ base_url = text_input(
272
+ "Introduce la URL pública de tu webhook",
273
+ default=os.environ.get("BASE_URL", ""),
274
+ required=False,
275
+ ).strip()
276
+ return {
277
+ "mode": "manual",
278
+ "base_url": base_url.rstrip("/"),
279
+ "tunnel_pid": "",
280
+ "tunnel_command": "",
281
+ }
282
+
283
+
284
+ def _local_ip() -> str:
285
+ try:
286
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
287
+ sock.connect(("8.8.8.8", 80))
288
+ return sock.getsockname()[0]
289
+ except Exception:
290
+ return "127.0.0.1"
291
+
292
+
293
+ def _configure_dashboard(port: int) -> dict:
294
+ options = [
295
+ "Solo local (localhost)",
296
+ "Red/IP externa de este dispositivo",
297
+ "URL pública personalizada",
298
+ "No configurar dashboard ahora",
299
+ ]
300
+ choice = select_menu(options, title="¿Cómo quieres exponer la página web de Conny?")
301
+ if choice == 0:
302
+ return {
303
+ "host": "127.0.0.1",
304
+ "dashboard_url": f"http://localhost:{port}/dashboard",
305
+ "public_dashboard_url": "",
306
+ }
307
+ if choice == 1:
308
+ ip = _local_ip()
309
+ return {
310
+ "host": "0.0.0.0",
311
+ "dashboard_url": f"http://{ip}:{port}/dashboard",
312
+ "public_dashboard_url": f"http://{ip}:{port}/dashboard",
313
+ }
314
+ if choice == 2:
315
+ url = text_input("Introduce la URL pública del dashboard", default="", required=False).strip().rstrip("/")
316
+ return {
317
+ "host": "0.0.0.0",
318
+ "dashboard_url": url,
319
+ "public_dashboard_url": url,
320
+ }
321
+ return {
322
+ "host": "127.0.0.1",
323
+ "dashboard_url": "",
324
+ "public_dashboard_url": "",
325
+ }
326
+
327
+
148
328
  def run_wizard():
149
329
 
150
330
  clear()
@@ -198,27 +378,27 @@ def run_wizard():
198
378
  print(f" {line}")
199
379
 
200
380
  print()
201
- print(f" {C_PRIMARY}{B1}Conny CLI {VERSION}{R} · {C_ACCENT}Autonomous Dynamic Receptionist{R} · {C_MUTED}⏱ Tiempo estimado: ~3 min{R}")
381
+ print(f" {C_PRIMARY}{B1}Conny CLI {VERSION}{R} · {C_ACCENT}Autonomous Dynamic Receptionist{R} · {C_MUTED}⏱ {_t('Tiempo estimado: ~3 min')}{R}")
202
382
  print(f" {C_MUTED}{'─' * 70}{R}")
203
383
  print()
204
384
 
205
385
 
206
386
 
207
387
  if not confirm("¿Comenzar configuración?"):
208
- print(f"\n {C_MUTED}Operación cancelada.{R}\n")
388
+ print(f"\n {C_MUTED}{_t('Operación cancelada.')}{R}\n")
209
389
  return
210
390
 
211
- TOTAL_STEPS = 7
391
+ TOTAL_STEPS = 9
212
392
  ctx = ""
213
393
 
214
394
  # 0. Language (Optional pre-step)
215
395
  clear()
216
396
  print(f"\n {C_PRIMARY}🌐 {_t('Selección de idioma')}{R}")
217
397
  i = select_menu([l[1] for l in LANGUAGES], title="Idioma preferido para Conny")
218
- i = select_menu([l[1] for l in LANGUAGES], title="Idioma preferido para Conny")
219
398
  lang = LANGUAGES[i][0]
220
399
  global CURRENT_LANG
221
400
  CURRENT_LANG = lang
401
+ _persist_language(lang)
222
402
 
223
403
  # 1. Identity
224
404
  clear()
@@ -241,9 +421,20 @@ def run_wizard():
241
421
  i = select_menu([c[1] for c in CHANNELS], title="Canal Principal de Acceso")
242
422
  channel = CHANNELS[i][0]
243
423
 
244
- # 4. Intelligence
424
+ # 4. Gateway / Webhook
425
+ port = _next_available_port()
245
426
  clear()
246
- step_header(4, TOTAL_STEPS, "Motor de Inteligencia", ctx)
427
+ step_header(4, TOTAL_STEPS, "Gateway público", f"{ctx}Canal: {channel} | Puerto: {port}")
428
+ gateway = _configure_gateway(port)
429
+
430
+ # 5. Web dashboard
431
+ clear()
432
+ step_header(5, TOTAL_STEPS, "Dashboard web", f"{ctx}Puerto: {port}")
433
+ dashboard = _configure_dashboard(port)
434
+
435
+ # 6. Intelligence
436
+ clear()
437
+ step_header(6, TOTAL_STEPS, "Motor de Inteligencia", ctx)
247
438
 
248
439
  # Level 1
249
440
  llm_types = [
@@ -322,15 +513,15 @@ def run_wizard():
322
513
  provider_id = "manual"
323
514
  model_id = text_input("Model ID")
324
515
 
325
- # 5. Personality
516
+ # 7. Personality
326
517
  clear()
327
- step_header(5, TOTAL_STEPS, "Humanización y Tono", ctx)
518
+ step_header(7, TOTAL_STEPS, "Humanización y Tono", ctx)
328
519
  i = select_menu([t[1] for t in TONES], title="Perfil de Voz")
329
520
  tone = TONES[i][0]
330
521
 
331
- # 6. Credentials
522
+ # 8. Credentials
332
523
  clear()
333
- step_header(6, TOTAL_STEPS, "Infraestructura y Secretos", ctx)
524
+ step_header(8, TOTAL_STEPS, "Infraestructura y Secretos", ctx)
334
525
 
335
526
  secrets_map = {}
336
527
 
@@ -371,9 +562,9 @@ def run_wizard():
371
562
  secrets_map["WA_CLOUD_TOKEN"] = text_input("WA Cloud API Token", is_password=True)
372
563
  secrets_map["WA_PHONE_NUMBER_ID"] = text_input("Phone Number ID")
373
564
 
374
- # 7. Confirmation
565
+ # 9. Confirmation
375
566
  clear()
376
- step_header(7, TOTAL_STEPS, "Verificación Final")
567
+ step_header(9, TOTAL_STEPS, "Verificación Final")
377
568
 
378
569
  print(f" ┌────────────────────────────────────────────────────────┐")
379
570
  print(f" │ {C_PRIMARY}{B1}Resumen de Configuración{R} │")
@@ -385,56 +576,58 @@ def run_wizard():
385
576
  print(f" │ {C_MUTED}Modelo:{R} {model_id.ljust(44)}│")
386
577
  print(f" │ {C_MUTED}Voz:{R} {tone.ljust(44)}│")
387
578
  print(f" │ {C_MUTED}Secretos:{R} {str(len(secrets_map)).ljust(44)}│")
579
+ print(f" │ {C_MUTED}BASE_URL:{R} {(gateway.get('base_url') or 'pending').ljust(44)[:44]}│")
580
+ print(f" │ {C_MUTED}Dashboard:{R} {(dashboard.get('dashboard_url') or 'local only').ljust(44)[:44]}│")
388
581
  print(f" └────────────────────────────────────────────────────────┘\n")
389
582
 
390
583
  if not confirm("¿Procesar e Implementar Infraestructura?"):
391
584
  print(f"\n {C_MUTED}Cancelado.{R}\n")
392
585
  return
393
586
 
394
- print(f"\n {C_PRIMARY}Creando recursos...{R}")
395
- _create(name, instance_id, sector, channel, provider_id, model_id, tone, secrets_map, lang)
587
+ print(f"\n {C_PRIMARY}{_t('Creando recursos...')}{R}")
588
+ _create(name, instance_id, sector, channel, provider_id, model_id, tone, secrets_map, lang, port, gateway, dashboard)
396
589
 
397
590
  print(f"""
398
- {C_SUCCESS}{B1}🚀 Infraestructura Desplegada Exitosamente{R}
591
+ {C_SUCCESS}{B1}🚀 {_t('Infraestructura Desplegada Exitosamente')}{R}
399
592
 
400
- {C_MUTED}Directorio:{R} {INSTANCES_DIR / instance_id}
401
- {C_MUTED}Control:{R} {C_PRIMARY}conny status {instance_id}{R}
402
- {C_MUTED}Check:{R} {C_PRIMARY}conny doctor{R}
403
- {C_MUTED}Lanzar:{R} {C_PRIMARY}pm2 start {INSTANCES_DIR / instance_id}/run.sh --name conny-{instance_id}{R}
593
+ {C_MUTED}{_t('Directorio:')}{R} {INSTANCES_DIR / instance_id}
594
+ {C_MUTED}{_t('Control:')}{R} {C_PRIMARY}conny status {instance_id}{R}
595
+ {C_MUTED}{_t('Check:')}{R} {C_PRIMARY}conny doctor{R}
596
+ {C_MUTED}{_t('Lanzar:')}{R} {C_PRIMARY}pm2 start {INSTANCES_DIR / instance_id}/run.sh --name conny-{instance_id}{R}
404
597
  """)
405
598
 
406
599
  # Post-onboarding commands
407
600
  if is_local and provider_id == "ollama":
408
601
  print(f" {C_WARNING}💡 Asegúrate de descargar el modelo: `ollama run {model_id}`{R}\n")
409
602
 
410
- def _create(name, iid, sector, channel, provider, model_id, tone, secrets_map, lang):
603
+ def _create(name, iid, sector, channel, provider, model_id, tone, secrets_map, lang, port, gateway, dashboard):
411
604
  idir = INSTANCES_DIR / iid
412
605
  idir.mkdir(parents=True, exist_ok=True)
413
606
 
414
- existing_ports = []
415
- if INSTANCES_DIR.exists():
416
- for d in INSTANCES_DIR.iterdir():
417
- env = d / ".env"
418
- if env.exists():
419
- for line in env.read_text().splitlines():
420
- if line.startswith("PORT="):
421
- try:
422
- parts = line.split("=")
423
- if len(parts) > 1:
424
- existing_ports.append(int(parts[1]))
425
- except: pass
426
-
427
- port = max(existing_ports + [8003]) + 1
428
607
  webhook_secret = f"conny_{iid}_{secrets.token_hex(6)}"
608
+ base_url = str((gateway or {}).get("base_url") or "").rstrip("/")
609
+ tunnel_command = str((gateway or {}).get("tunnel_command") or "").replace('"', '\\"')
610
+ dashboard = dashboard or {}
611
+ host = str(dashboard.get("host") or "127.0.0.1").strip()
612
+ dashboard_url = str(dashboard.get("dashboard_url") or "").rstrip("/")
613
+ public_dashboard_url = str(dashboard.get("public_dashboard_url") or "").rstrip("/")
429
614
 
430
615
  env_lines = [
431
616
  f"INSTANCE_ID={iid}",
432
617
  f"PORT={port}",
618
+ f"HOST={host}",
619
+ f"BASE_URL={base_url}",
620
+ f"PUBLIC_BASE_URL={base_url}",
621
+ f"DASHBOARD_URL={dashboard_url}",
622
+ f"PUBLIC_DASHBOARD_URL={public_dashboard_url}",
433
623
  "DEMO_MODE=false",
434
624
  f"PLATFORM={channel}",
435
625
  f"SECTOR={sector}",
436
626
  f"BUSINESS_NAME=\"{name}\"",
437
627
  f"WEBHOOK_SECRET={webhook_secret}",
628
+ f"TUNNEL_PROVIDER={(gateway or {}).get('mode', '')}",
629
+ f"TUNNEL_PID={(gateway or {}).get('tunnel_pid', '')}",
630
+ f"TUNNEL_COMMAND=\"{tunnel_command}\"",
438
631
  f"LLM_PROVIDER={provider}",
439
632
  f"LLM_MODEL={model_id}",
440
633
  "DEBUG=false"
@@ -473,7 +666,10 @@ llm:
473
666
 
474
667
  for f in core:
475
668
  src = CONNY_DIR / f
476
- if src.exists(): shutil.copy2(src, idir / f)
669
+ if src.exists():
670
+ dst = idir / f
671
+ dst.parent.mkdir(parents=True, exist_ok=True)
672
+ shutil.copy2(src, dst)
477
673
 
478
674
  for d in ["soul", "teachings", "memory_store", "knowledge_gaps", "integrations/vault", "logs"]:
479
675
  (idir / d).mkdir(parents=True, exist_ok=True)
@@ -486,10 +682,27 @@ llm:
486
682
  "status": "configured",
487
683
  "timestamp": datetime.now().isoformat(),
488
684
  "instance": iid,
489
- "provider": provider
685
+ "provider": provider,
686
+ "port": port,
687
+ "base_url": base_url,
688
+ "dashboard": {
689
+ "host": host,
690
+ "url": dashboard_url,
691
+ "public_url": public_dashboard_url,
692
+ },
693
+ "tunnel": {
694
+ "provider": (gateway or {}).get("mode", ""),
695
+ "pid": (gateway or {}).get("tunnel_pid", ""),
696
+ "command": (gateway or {}).get("tunnel_command", ""),
697
+ },
490
698
  }
491
699
  (idir / "conny.state.json").write_text(json.dumps(state, indent=2))
492
700
 
701
+ if set_active_instance:
702
+ set_active_instance(iid)
703
+ if mirror_instance_env_to_base:
704
+ mirror_instance_env_to_base(iid)
705
+
493
706
  def _slug(name):
494
707
  s = name.lower().strip()
495
708
  for a, b in [("á","a"),("é","e"),("í","i"),("ó","o"),("ú","u"),("ñ","n")]: