ltcai 6.1.0 → 6.3.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 (70) hide show
  1. package/README.md +36 -38
  2. package/docs/CHANGELOG.md +75 -1
  3. package/frontend/src/App.tsx +3 -1286
  4. package/frontend/src/components/LanguageSwitcher.tsx +23 -0
  5. package/frontend/src/components/ProductFlow.tsx +32 -669
  6. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  7. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  8. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  9. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  10. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  11. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -0
  12. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  13. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  14. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  15. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  16. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  17. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  18. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  19. package/frontend/src/features/brain/BrainHome.tsx +232 -0
  20. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  21. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  22. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  23. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  24. package/frontend/src/features/brain/brainData.ts +98 -0
  25. package/frontend/src/features/brain/graphLayout.ts +26 -0
  26. package/frontend/src/features/brain/types.ts +44 -0
  27. package/frontend/src/features/review/ReviewCard.tsx +3 -1
  28. package/frontend/src/features/review/ReviewInbox.tsx +11 -1
  29. package/frontend/src/i18n.ts +290 -0
  30. package/frontend/src/pages/Brain.tsx +63 -5
  31. package/frontend/src/pages/Capture.tsx +102 -13
  32. package/frontend/src/pages/Library.tsx +72 -6
  33. package/frontend/src/styles.css +220 -0
  34. package/lattice_brain/__init__.py +1 -1
  35. package/lattice_brain/runtime/multi_agent.py +1 -1
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/tools.py +50 -23
  38. package/latticeai/app_factory.py +59 -76
  39. package/latticeai/cli/entrypoint.py +283 -0
  40. package/latticeai/core/marketplace.py +1 -1
  41. package/latticeai/core/workspace_os.py +1 -1
  42. package/latticeai/integrations/__init__.py +0 -0
  43. package/latticeai/integrations/telegram_bot.py +1009 -0
  44. package/latticeai/runtime/chat_wiring.py +119 -0
  45. package/latticeai/runtime/lifespan_runtime.py +1 -1
  46. package/latticeai/runtime/model_wiring.py +40 -0
  47. package/latticeai/runtime/platform_runtime_wiring.py +86 -0
  48. package/latticeai/runtime/review_wiring.py +37 -0
  49. package/latticeai/runtime/router_registration.py +49 -30
  50. package/latticeai/runtime/tail_wiring.py +21 -0
  51. package/latticeai/services/p_reinforce.py +258 -0
  52. package/latticeai/services/router_context.py +52 -0
  53. package/ltcai_cli.py +7 -279
  54. package/p_reinforce.py +4 -255
  55. package/package.json +3 -2
  56. package/scripts/check_i18n_literals.mjs +52 -0
  57. package/scripts/release_smoke.py +133 -0
  58. package/scripts/wheel_smoke.py +4 -0
  59. package/src-tauri/Cargo.lock +1 -1
  60. package/src-tauri/Cargo.toml +1 -1
  61. package/src-tauri/tauri.conf.json +1 -1
  62. package/static/app/asset-manifest.json +5 -5
  63. package/static/app/assets/index-D76dWuQk.js +16 -0
  64. package/static/app/assets/index-D76dWuQk.js.map +1 -0
  65. package/static/app/assets/index-Div5vMlq.css +2 -0
  66. package/static/app/index.html +2 -2
  67. package/telegram_bot.py +9 -1002
  68. package/static/app/assets/index-B744yblP.css +0 -2
  69. package/static/app/assets/index-DYaUKNfl.js +0 -16
  70. package/static/app/assets/index-DYaUKNfl.js.map +0 -1
@@ -17,10 +17,14 @@ from __future__ import annotations
17
17
  import threading
18
18
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
19
19
 
20
- from latticeai.runtime.automation_runtime import build_automation_runtime
21
20
  from latticeai.runtime.app_context_runtime import build_app_context
22
21
  from latticeai.runtime.bootstrap import build_session_runtime
23
22
  from latticeai.runtime.brain_runtime import build_brain_runtime
23
+ from latticeai.runtime.chat_wiring import (
24
+ build_chat_agent_runtime_from_context,
25
+ build_interaction_contexts,
26
+ maybe_build_telegram_chat_mirror,
27
+ )
24
28
  from latticeai.runtime.config_runtime import build_config_runtime
25
29
  from latticeai.runtime.context_runtime import build_context_runtime
26
30
  from latticeai.runtime.hooks_runtime import (
@@ -29,11 +33,16 @@ from latticeai.runtime.hooks_runtime import (
29
33
  build_hooks_runtime,
30
34
  )
31
35
  from latticeai.runtime.lifespan_runtime import build_lifespan_runtime
36
+ from latticeai.runtime.model_wiring import (
37
+ configure_model_runtime_from_context,
38
+ register_model_runtime_routers,
39
+ )
32
40
  from latticeai.runtime.platform_services_runtime import (
33
41
  build_brain_network,
34
- build_model_service,
35
42
  )
43
+ from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
36
44
  from latticeai.runtime.persistence_runtime import build_persistence_runtime
45
+ from latticeai.runtime.review_wiring import build_review_run_now_runner
37
46
  from latticeai.runtime.router_registration import (
38
47
  build_auth_admin_security_router_bundle,
39
48
  build_static_routes_bundle,
@@ -44,6 +53,7 @@ from latticeai.runtime.router_registration import (
44
53
  register_review_and_brain_tail_routers,
45
54
  )
46
55
  from latticeai.runtime.security_runtime import build_security_runtime
56
+ from latticeai.runtime.tail_wiring import register_tail_runtime_routers
47
57
  from latticeai.runtime.web_runtime import build_web_runtime
48
58
 
49
59
  if TYPE_CHECKING: # imports for annotations only — keep module import light
@@ -145,7 +155,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
145
155
  from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
146
156
  from latticeai.api.health import create_health_router
147
157
  # ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
148
- from latticeai.services.platform_runtime import PlatformRuntime
149
158
  from latticeai.api.plugins import create_plugins_router
150
159
  from latticeai.api.workflow_designer import create_workflow_designer_router
151
160
  from latticeai.api.agents import create_agents_router
@@ -189,7 +198,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
189
198
  _get_combined_registry,
190
199
  _fetch_skills_marketplace, install_skill, SKILLS_DIR,
191
200
  )
192
- from p_reinforce import PReinforceGardener
201
+ from latticeai.services.p_reinforce import PReinforceGardener
193
202
  from setup_wizard import get_recommendations, scan_environment
194
203
  from tools import ensure_agent_root, execute_tool, knowledge_save
195
204
 
@@ -1089,7 +1098,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1089
1098
  OPEN_REGISTRATION = CONFIG.open_registration
1090
1099
  INVITE_CODE = CONFIG.invite_code
1091
1100
  INVITE_GATE_ENABLED = CONFIG.invite_gate_enabled
1092
- configure_model_runtime(
1101
+ configure_model_runtime_from_context(
1102
+ configure_model_runtime=configure_model_runtime,
1093
1103
  router=router,
1094
1104
  APP_MODE=APP_MODE,
1095
1105
  DEFAULT_HOST=DEFAULT_HOST,
@@ -1236,12 +1246,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1236
1246
  # ── Telegram chat mirror: registered only when ENABLE_TELEGRAM is truthy.
1237
1247
  # latticeai.api.chat no longer imports telegram_bot (a 45KB module that
1238
1248
  # mutates os.environ at import); it calls this injected callback instead.
1239
- on_chat_message = None
1240
- if ENABLE_TELEGRAM:
1241
- def _telegram_chat_mirror(role: str, text: str, source: Optional[str] = None) -> None:
1242
- from telegram_bot import broadcast_web_chat
1243
- _spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
1244
- on_chat_message = _telegram_chat_mirror
1249
+ on_chat_message = maybe_build_telegram_chat_mirror(
1250
+ enable_telegram=ENABLE_TELEGRAM,
1251
+ spawn=_spawn,
1252
+ )
1245
1253
 
1246
1254
  def _recent_chat_context(
1247
1255
  limit: int = 10,
@@ -1257,7 +1265,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1257
1265
  conversation_id=conversation_id,
1258
1266
  )
1259
1267
 
1260
- CHAT_AGENT_RUNTIME = build_agent_runtime(
1268
+ CHAT_AGENT_RUNTIME = build_chat_agent_runtime_from_context(
1269
+ build_agent_runtime=build_agent_runtime,
1261
1270
  model_router=router,
1262
1271
  execute_tool=execute_tool,
1263
1272
  recent_chat_context=_recent_chat_context,
@@ -1338,20 +1347,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1338
1347
 
1339
1348
 
1340
1349
  # ── v2 Agentic Workspace Platform: cross-system wiring ───────────────────────
1341
- # All cross-subsystem closures live in latticeai.services.platform_runtime to
1342
- # keep this assembly file lean; server_app only constructs it and mounts routers.
1343
- def _llm_generate_sync(message: str, context: str = "", max_tokens: int = 1024, temperature: float = 0.1) -> str:
1344
- # Synchronous model bridge for the orchestrator's role runner. Safe
1345
- # because the agents run endpoint executes start() in a worker thread
1346
- # (asyncio.to_thread), where no event loop is running.
1347
- import asyncio as _asyncio
1348
-
1349
- return str(_asyncio.run(router.generate(
1350
- message, context=context, max_tokens=max_tokens, temperature=temperature,
1351
- )))
1352
-
1353
- PLATFORM = PlatformRuntime(
1354
- store=WORKSPACE_OS,
1350
+ _platform_automation_runtime = build_platform_automation_runtime(
1351
+ model_router=router,
1352
+ workspace_store=WORKSPACE_OS,
1355
1353
  workspace_service=WORKSPACE_SERVICE,
1356
1354
  plugin_registry=PLUGIN_REGISTRY,
1357
1355
  get_current_user=get_current_user,
@@ -1359,23 +1357,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1359
1357
  workspace_scope_from_request=_workspace_scope_from_request,
1360
1358
  get_tool_permission=get_tool_permission,
1361
1359
  hooks=HOOKS_REGISTRY,
1362
- llm_generate=_llm_generate_sync,
1363
- llm_available=lambda: bool(getattr(router, "current_model_id", None)),
1364
1360
  agent_registry=AGENT_REGISTRY,
1365
- )
1366
-
1367
- _automation_runtime = build_automation_runtime(
1368
- store=WORKSPACE_OS,
1369
- platform=PLATFORM,
1370
1361
  data_dir=DATA_DIR,
1371
- workspace_graph=_workspace_graph,
1372
1362
  append_audit_event=append_audit_event,
1373
- hooks=HOOKS_REGISTRY,
1374
1363
  )
1375
- REVIEW_QUEUE = _automation_runtime["REVIEW_QUEUE"]
1376
- TRIGGER_SERVICE = _automation_runtime["TRIGGER_SERVICE"]
1377
- AGENT_RUNTIME = _automation_runtime["AGENT_RUNTIME"]
1378
- RUN_EXECUTOR = _automation_runtime["RUN_EXECUTOR"]
1364
+ _llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
1365
+ PLATFORM = _platform_automation_runtime["PLATFORM"]
1366
+ _automation_runtime = _platform_automation_runtime["_automation_runtime"]
1367
+ REVIEW_QUEUE = _platform_automation_runtime["REVIEW_QUEUE"]
1368
+ TRIGGER_SERVICE = _platform_automation_runtime["TRIGGER_SERVICE"]
1369
+ AGENT_RUNTIME = _platform_automation_runtime["AGENT_RUNTIME"]
1370
+ RUN_EXECUTOR = _platform_automation_runtime["RUN_EXECUTOR"]
1379
1371
  bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
1380
1372
  app.state.run_executor = RUN_EXECUTOR
1381
1373
  app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
@@ -1421,22 +1413,19 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1421
1413
  # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1422
1414
  # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1423
1415
  # Heavier engine mutation endpoints remain below in server_app.
1424
- MODEL_SERVICE = build_model_service(
1416
+ MODEL_SERVICE = register_model_runtime_routers(
1417
+ app=app,
1418
+ create_health_router=create_health_router,
1419
+ create_models_router=create_models_router,
1420
+ register_health_and_model_routers=register_health_and_model_routers,
1425
1421
  model_router=router,
1426
1422
  runtime_features=runtime_features,
1427
- is_public=IS_PUBLIC_MODE,
1428
- )
1429
- register_health_and_model_routers(
1430
- app,
1431
- create_health_router=create_health_router,
1432
- model_service=MODEL_SERVICE,
1423
+ is_public_mode=IS_PUBLIC_MODE,
1433
1424
  engine_status=engine_status,
1434
1425
  get_current_user=get_current_user,
1435
1426
  require_auth=REQUIRE_AUTH,
1436
1427
  app_version=APP_VERSION,
1437
1428
  app_mode=APP_MODE,
1438
- create_models_router=create_models_router,
1439
- model_router=router,
1440
1429
  require_user=require_user,
1441
1430
  load_users=load_users,
1442
1431
  get_user_role=get_user_role,
@@ -1455,7 +1444,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1455
1444
  engine_model_catalog=ENGINE_MODEL_CATALOG,
1456
1445
  model_engine_aliases=MODEL_ENGINE_ALIASES,
1457
1446
  cloud_verify_ttl_seconds=CLOUD_VERIFY_TTL_SECONDS,
1458
- is_public_mode=IS_PUBLIC_MODE,
1459
1447
  allow_local_models=ALLOW_LOCAL_MODELS,
1460
1448
  )
1461
1449
 
@@ -1478,21 +1466,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1478
1466
  return None
1479
1467
  return PLATFORM.allowed_scopes(user)
1480
1468
 
1481
- register_interaction_routers(
1482
- app,
1483
- create_chat_router=create_chat_router,
1484
- context=context,
1485
- create_search_router=create_search_router,
1486
- search_service=SEARCH_SERVICE,
1487
- allowed_workspaces_for=_allowed_workspaces_for,
1488
- require_user=require_user,
1489
- embedding_info=_embedding_info,
1490
- create_tools_router=create_tools_router,
1491
- ingestion_pipeline=INGESTION_PIPELINE,
1469
+ tool_router_context, interaction_router_context = build_interaction_contexts(
1492
1470
  config=CONFIG,
1471
+ ingestion_pipeline=INGESTION_PIPELINE,
1493
1472
  data_dir=DATA_DIR,
1494
1473
  static_dir=STATIC_DIR,
1495
1474
  model_router=router,
1475
+ require_user=require_user,
1496
1476
  require_admin=require_admin,
1497
1477
  get_current_user=get_current_user,
1498
1478
  clear_history=clear_history,
@@ -1510,34 +1490,37 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1510
1490
  install_mcp=install_mcp,
1511
1491
  mcp_public_item=mcp_public_item,
1512
1492
  hooks=HOOKS_REGISTRY,
1513
- create_hooks_router=create_hooks_router,
1514
- create_agent_registry_router=create_agent_registry_router,
1493
+ chat_context=context,
1494
+ search_service=SEARCH_SERVICE,
1495
+ allowed_workspaces_for=_allowed_workspaces_for,
1496
+ embedding_info=_embedding_info,
1515
1497
  agent_registry=AGENT_REGISTRY,
1516
- create_memory_router=create_memory_router,
1517
1498
  memory_service=MEMORY_SERVICE,
1518
1499
  platform=PLATFORM,
1519
1500
  )
1501
+ register_interaction_routers(
1502
+ app,
1503
+ interaction_context=interaction_router_context,
1504
+ create_chat_router=create_chat_router,
1505
+ create_search_router=create_search_router,
1506
+ create_tools_router=create_tools_router,
1507
+ create_hooks_router=create_hooks_router,
1508
+ create_agent_registry_router=create_agent_registry_router,
1509
+ create_memory_router=create_memory_router,
1510
+ )
1520
1511
 
1521
1512
  from latticeai.api.review_queue import create_review_queue_router
1513
+ run_review_item = build_review_run_now_runner(PLATFORM, HTTPException)
1522
1514
 
1523
- def _run_review_item(item, *, user_email, scope):
1524
- """run_now: re-execute the suggestion's source workflow (preview/regenerate)."""
1525
- wf_id = (item.get("payload") or {}).get("workflow_id") or (item.get("provenance") or {}).get("workflow_id")
1526
- if not wf_id:
1527
- raise HTTPException(status_code=409, detail="review item has no workflow to run")
1528
- return PLATFORM.run_workflow_by_id(
1529
- wf_id, user_email, scope, with_agent=False,
1530
- inputs={"__review_item__": item.get("id")},
1531
- )
1532
-
1533
- BRAIN_NETWORK = register_review_and_brain_tail_routers(
1534
- app,
1515
+ BRAIN_NETWORK = register_tail_runtime_routers(
1516
+ app=app,
1535
1517
  create_review_queue_router=create_review_queue_router,
1518
+ register_review_and_brain_tail_routers=register_review_and_brain_tail_routers,
1536
1519
  review_queue=REVIEW_QUEUE,
1537
1520
  require_user=require_user,
1538
1521
  gate_read=PLATFORM.gate_read,
1539
1522
  gate_write=PLATFORM.gate_write,
1540
- run_review_item=_run_review_item,
1523
+ run_review_item=run_review_item,
1541
1524
  append_audit_event=append_audit_event,
1542
1525
  create_browser_router=create_browser_router,
1543
1526
  ingestion_pipeline=INGESTION_PIPELINE,
@@ -0,0 +1,283 @@
1
+ """Command line entrypoint for Lattice AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import platform
8
+ import re
9
+ import shutil
10
+ import socket
11
+ import stat
12
+ import subprocess
13
+ import sys
14
+ import threading
15
+ import time
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ from latticeai.cli.runtime import (
20
+ _apply_extra_path,
21
+ _has_module,
22
+ _load_env_file,
23
+ )
24
+
25
+ def _local_ips() -> list[str]:
26
+ ips: list[str] = []
27
+ try:
28
+ hostname = socket.gethostname()
29
+ for info in socket.getaddrinfo(hostname, None):
30
+ addr = info[4][0]
31
+ if ":" not in addr and not addr.startswith("127."):
32
+ if addr not in ips:
33
+ ips.append(addr)
34
+ except Exception:
35
+ pass
36
+ if not ips:
37
+ try:
38
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
39
+ s.connect(("8.8.8.8", 80))
40
+ ips.append(s.getsockname()[0])
41
+ s.close()
42
+ except Exception:
43
+ pass
44
+ return ips
45
+
46
+
47
+ def _print_banner(host: str, port: int, tunnel_url: str | None = None) -> None:
48
+ local_url = f"http://localhost:{port}"
49
+ print()
50
+ print("=" * 56)
51
+ print(" Lattice AI is running")
52
+ print(f" Local: {local_url}")
53
+ if host == "0.0.0.0":
54
+ for ip in _local_ips():
55
+ print(f" Network: http://{ip}:{port}")
56
+ print()
57
+ print(" Other devices on the same Wi-Fi can open the")
58
+ print(" Network URL above in their browser.")
59
+ print(" On iPad/Android: browser menu → 'Add to Home Screen'")
60
+ if tunnel_url:
61
+ print()
62
+ print(f" Tunnel: {tunnel_url}")
63
+ print(" Anyone on the internet can access via the Tunnel URL.")
64
+ print("=" * 56)
65
+ print()
66
+
67
+
68
+ def doctor() -> int:
69
+ checks = [
70
+ ("Python 3.11+", sys.version_info >= (3, 11), sys.version.split()[0], True),
71
+ ("FastAPI", _has_module("fastapi"), "required server dependency", True),
72
+ ("Uvicorn", _has_module("uvicorn"), "required server dependency", True),
73
+ ("OpenAI SDK", _has_module("openai"), "required for cloud providers", False),
74
+ ("MLX", _has_module("mlx"), "required for Apple Silicon multimodal models", False),
75
+ ("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma-4/VLM models", False),
76
+ ("Ollama binary", shutil.which("ollama") is not None, "optional local-server engine", False),
77
+ ]
78
+ data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or Path.home() / ".ltcai")
79
+ static_dir = Path(os.getenv("LATTICEAI_STATIC_DIR") or Path(__file__).resolve().parent / "static")
80
+ checks.extend([
81
+ ("Data dir", data_dir.exists() or data_dir.parent.exists(), str(data_dir), True),
82
+ ("Static UI", static_dir.exists(), str(static_dir), True),
83
+ ])
84
+
85
+ ok = True
86
+ for label, passed, detail, required in checks:
87
+ icon = "OK" if passed else ("MISS" if required else "OPTIONAL")
88
+ print(f"[{icon}] {label}: {detail}")
89
+ ok = ok and (passed or not required)
90
+
91
+ cloud_keys = ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY"]
92
+ configured = [key for key in cloud_keys if os.getenv(key)]
93
+ print(f"[INFO] Cloud keys configured: {', '.join(configured) if configured else 'none'}")
94
+ return 0 if ok else 1
95
+
96
+
97
+ # ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
98
+
99
+ def _cloudflared_url() -> str:
100
+ system = sys.platform
101
+ machine = platform.machine().lower()
102
+ base = "https://github.com/cloudflare/cloudflared/releases/latest/download"
103
+ if system == "darwin":
104
+ arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
105
+ return f"{base}/cloudflared-darwin-{arch}"
106
+ if system == "win32":
107
+ return f"{base}/cloudflared-windows-amd64.exe"
108
+ arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
109
+ return f"{base}/cloudflared-linux-{arch}"
110
+
111
+
112
+ def _cloudflared_bin() -> Path:
113
+ suffix = ".exe" if sys.platform == "win32" else ""
114
+ return Path.home() / ".latticeai" / "bin" / f"cloudflared{suffix}"
115
+
116
+
117
+ def _ensure_cloudflared() -> str:
118
+ found = shutil.which("cloudflared")
119
+ if found:
120
+ return found
121
+ dest = _cloudflared_bin()
122
+ if dest.exists():
123
+ return str(dest)
124
+ dest.parent.mkdir(parents=True, exist_ok=True)
125
+ url = _cloudflared_url()
126
+ print(" cloudflared not found — downloading from GitHub...")
127
+ try:
128
+ urllib.request.urlretrieve(url, dest)
129
+ if sys.platform != "win32":
130
+ dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
131
+ print(f" cloudflared installed at {dest}")
132
+ return str(dest)
133
+ except Exception as e:
134
+ print(f" cloudflared download failed: {e}")
135
+ print(" Install manually: https://developers.cloudflare.com/cloudflared/install")
136
+ return ""
137
+
138
+
139
+ def _send_telegram(token: str, chat_id: str, text: str) -> None:
140
+ try:
141
+ data = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode()
142
+ req = urllib.request.Request(
143
+ f"https://api.telegram.org/bot{token}/sendMessage",
144
+ data=data,
145
+ )
146
+ urllib.request.urlopen(req, timeout=10)
147
+ except Exception:
148
+ pass
149
+
150
+
151
+ def _start_tunnel(port: int) -> str | None:
152
+
153
+ bin_path = _ensure_cloudflared()
154
+ if not bin_path:
155
+ return None
156
+
157
+ log_path = Path.home() / ".latticeai" / "tunnel.log"
158
+ log_path.parent.mkdir(parents=True, exist_ok=True)
159
+
160
+ proc = subprocess.Popen(
161
+ [bin_path, "tunnel", "--url", f"http://localhost:{port}"],
162
+ stdout=open(log_path, "w"),
163
+ stderr=subprocess.STDOUT,
164
+ )
165
+
166
+ # Wait for the public URL (up to 30s)
167
+ pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
168
+ deadline = time.time() + 30
169
+ url: str | None = None
170
+ while time.time() < deadline:
171
+ time.sleep(0.5)
172
+ try:
173
+ text = log_path.read_text(errors="replace")
174
+ m = pattern.search(text)
175
+ if m:
176
+ url = m.group(0)
177
+ break
178
+ except Exception:
179
+ pass
180
+
181
+ if not url:
182
+ return None
183
+
184
+ # Telegram notification if configured
185
+ token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
186
+ chat_id = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
187
+ if token and chat_id:
188
+ msg = (
189
+ f"✅ Lattice AI 시작됨\n\n"
190
+ f"🌐 외부 URL: {url}\n"
191
+ f"🏠 로컬: http://localhost:{port}"
192
+ )
193
+ threading.Thread(target=_send_telegram, args=(token, chat_id, msg), daemon=True).start()
194
+
195
+ return url
196
+
197
+
198
+ # ─────────────────────────────────────────────────────────────────────────────
199
+
200
+ def main() -> None:
201
+ app_dir = Path(__file__).resolve().parent
202
+ _load_env_file(app_dir / ".env")
203
+ _apply_extra_path()
204
+
205
+ parser = argparse.ArgumentParser(prog="LTCAI", description="Run the Lattice AI local server.")
206
+ subparsers = parser.add_subparsers(dest="command")
207
+ subparsers.add_parser("doctor", help="Check local runtime dependencies and configuration.")
208
+ parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "127.0.0.1")
209
+ parser.add_argument("--port", type=int, default=int(os.getenv("LATTICEAI_PORT") or "4825"))
210
+ parser.add_argument("--reload", action="store_true", help="Enable uvicorn reload for local development.")
211
+ parser.add_argument(
212
+ "--tunnel",
213
+ action="store_true",
214
+ help="Open a public Cloudflare tunnel so anyone can access this server from the internet.",
215
+ )
216
+ args = parser.parse_args()
217
+
218
+ if args.command == "doctor":
219
+ raise SystemExit(doctor())
220
+
221
+ os.chdir(app_dir)
222
+
223
+ if not args.tunnel and os.getenv("LATTICEAI_TUNNEL", "").lower() in (
224
+ "1",
225
+ "true",
226
+ "yes",
227
+ ):
228
+ print(
229
+ " LATTICEAI_TUNNEL is ignored during default local startup; "
230
+ "restart with --tunnel to expose this server."
231
+ )
232
+
233
+ # --tunnel forces 0.0.0.0 so cloudflared can reach the server
234
+ if args.tunnel and args.host == "127.0.0.1":
235
+ args.host = "0.0.0.0"
236
+ os.environ.setdefault("LATTICEAI_HOST", "0.0.0.0")
237
+ os.environ.setdefault("LATTICEAI_CORS_ALLOW_NETWORK", "true")
238
+ os.environ.setdefault("LATTICEAI_REQUIRE_AUTH", "true")
239
+
240
+ # Keep the app config in sync with CLI flags. ``Config.from_env`` is the
241
+ # source of truth for /mode, /health.features, SSO defaults, and routers.
242
+ os.environ["LATTICEAI_HOST"] = str(args.host)
243
+ os.environ["LATTICEAI_PORT"] = str(args.port)
244
+
245
+ tunnel_url: str | None = None
246
+ if args.tunnel:
247
+ print()
248
+ print(" Starting Cloudflare tunnel...")
249
+ tunnel_url = _start_tunnel(args.port)
250
+ if not tunnel_url:
251
+ print(" ⚠️ Tunnel URL not obtained — server will start without tunnel.")
252
+
253
+ _print_banner(args.host, args.port, tunnel_url)
254
+
255
+ # Telegram startup notification (local start, tunnel handled separately inside _start_tunnel)
256
+ if not args.tunnel:
257
+ _tg_enabled = os.getenv("LATTICEAI_ENABLE_TELEGRAM", "").strip().lower() in ("1", "true", "yes", "on")
258
+ _tg_token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
259
+ _tg_chat = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
260
+ if _tg_enabled and _tg_token and _tg_chat:
261
+ _local_msg = (
262
+ f"✅ Lattice AI 시작됨\n\n"
263
+ f"🏠 로컬: http://localhost:{args.port}"
264
+ )
265
+ threading.Thread(
266
+ target=_send_telegram,
267
+ args=(_tg_token, _tg_chat, _local_msg),
268
+ daemon=True,
269
+ ).start()
270
+
271
+ import uvicorn
272
+
273
+ uvicorn.run(
274
+ "server:app",
275
+ host=args.host,
276
+ port=args.port,
277
+ reload=args.reload,
278
+ log_level="info",
279
+ )
280
+
281
+
282
+ if __name__ == "__main__":
283
+ main()
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "6.1.0"
14
+ MARKETPLACE_VERSION = "6.3.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "6.1.0"
22
+ WORKSPACE_OS_VERSION = "6.3.0"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type
File without changes