ltcai 6.1.0 → 6.2.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 (44) hide show
  1. package/README.md +33 -38
  2. package/docs/CHANGELOG.md +33 -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/ProductFlowScreens.tsx +688 -0
  7. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  8. package/frontend/src/features/brain/BrainHome.tsx +999 -0
  9. package/frontend/src/features/brain/brainData.ts +98 -0
  10. package/frontend/src/features/brain/graphLayout.ts +26 -0
  11. package/frontend/src/features/brain/types.ts +44 -0
  12. package/frontend/src/i18n.ts +198 -0
  13. package/frontend/src/styles.css +220 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/runtime/multi_agent.py +1 -1
  16. package/latticeai/__init__.py +1 -1
  17. package/latticeai/api/tools.py +50 -23
  18. package/latticeai/app_factory.py +36 -45
  19. package/latticeai/cli/entrypoint.py +283 -0
  20. package/latticeai/core/marketplace.py +1 -1
  21. package/latticeai/core/workspace_os.py +1 -1
  22. package/latticeai/integrations/__init__.py +0 -0
  23. package/latticeai/integrations/telegram_bot.py +1009 -0
  24. package/latticeai/runtime/lifespan_runtime.py +1 -1
  25. package/latticeai/runtime/platform_runtime_wiring.py +87 -0
  26. package/latticeai/runtime/router_registration.py +49 -30
  27. package/latticeai/services/p_reinforce.py +258 -0
  28. package/latticeai/services/router_context.py +52 -0
  29. package/ltcai_cli.py +7 -279
  30. package/p_reinforce.py +4 -255
  31. package/package.json +2 -1
  32. package/scripts/release_smoke.py +133 -0
  33. package/scripts/wheel_smoke.py +4 -0
  34. package/src-tauri/Cargo.lock +1 -1
  35. package/src-tauri/Cargo.toml +1 -1
  36. package/src-tauri/tauri.conf.json +1 -1
  37. package/static/app/asset-manifest.json +5 -5
  38. package/static/app/assets/{index-B744yblP.css → index-B2-1Gm0q.css} +1 -1
  39. package/static/app/assets/index-D91Rz5--.js +16 -0
  40. package/static/app/assets/index-D91Rz5--.js.map +1 -0
  41. package/static/app/index.html +2 -2
  42. package/telegram_bot.py +9 -1002
  43. package/static/app/assets/index-DYaUKNfl.js +0 -16
  44. package/static/app/assets/index-DYaUKNfl.js.map +0 -1
@@ -26,7 +26,8 @@ from latticeai.services.tool_dispatch import (
26
26
  get_tool_permission,
27
27
  list_tool_permissions,
28
28
  )
29
- from p_reinforce import BRAIN_DIR
29
+ from latticeai.services.router_context import ToolRouterContext
30
+ from latticeai.services.p_reinforce import BRAIN_DIR
30
31
  from tools import (
31
32
  AGENT_ROOT,
32
33
  ToolError,
@@ -177,30 +178,56 @@ class ToolGitShowRequest(BaseModel):
177
178
 
178
179
  def create_tools_router(
179
180
  *,
180
- config,
181
- ingestion_pipeline,
182
- data_dir: Path,
183
- static_dir: Path,
184
- model_router,
185
- require_user,
186
- require_admin,
187
- get_current_user,
188
- clear_history,
189
- append_audit_event,
190
- enforce_rate_limit,
191
- bytes_match_extension,
192
- classify_sensitive_message,
193
- save_to_history,
194
- enable_graph: bool,
195
- knowledge_graph,
196
- require_graph,
197
- local_kg_watcher,
198
- load_mcp_installs,
199
- recommend_mcps,
200
- install_mcp,
201
- mcp_public_item,
181
+ tool_context: ToolRouterContext | None = None,
182
+ config=None,
183
+ ingestion_pipeline=None,
184
+ data_dir: Path | None = None,
185
+ static_dir: Path | None = None,
186
+ model_router=None,
187
+ require_user=None,
188
+ require_admin=None,
189
+ get_current_user=None,
190
+ clear_history=None,
191
+ append_audit_event=None,
192
+ enforce_rate_limit=None,
193
+ bytes_match_extension=None,
194
+ classify_sensitive_message=None,
195
+ save_to_history=None,
196
+ enable_graph: bool | None = None,
197
+ knowledge_graph=None,
198
+ require_graph=None,
199
+ local_kg_watcher=None,
200
+ load_mcp_installs=None,
201
+ recommend_mcps=None,
202
+ install_mcp=None,
203
+ mcp_public_item=None,
202
204
  hooks=None,
203
205
  ) -> APIRouter:
206
+ if tool_context is not None:
207
+ config = tool_context.config
208
+ ingestion_pipeline = tool_context.ingestion_pipeline
209
+ data_dir = tool_context.data_dir
210
+ static_dir = tool_context.static_dir
211
+ model_router = tool_context.model_router
212
+ require_user = tool_context.require_user
213
+ require_admin = tool_context.require_admin
214
+ get_current_user = tool_context.get_current_user
215
+ clear_history = tool_context.clear_history
216
+ append_audit_event = tool_context.append_audit_event
217
+ enforce_rate_limit = tool_context.enforce_rate_limit
218
+ bytes_match_extension = tool_context.bytes_match_extension
219
+ classify_sensitive_message = tool_context.classify_sensitive_message
220
+ save_to_history = tool_context.save_to_history
221
+ enable_graph = tool_context.enable_graph
222
+ knowledge_graph = tool_context.knowledge_graph
223
+ require_graph = tool_context.require_graph
224
+ local_kg_watcher = tool_context.local_kg_watcher
225
+ load_mcp_installs = tool_context.load_mcp_installs
226
+ recommend_mcps = tool_context.recommend_mcps
227
+ install_mcp = tool_context.install_mcp
228
+ mcp_public_item = tool_context.mcp_public_item
229
+ hooks = tool_context.hooks
230
+
204
231
  api_router = APIRouter()
205
232
  HOOKS = hooks
206
233
  CONFIG = config
@@ -17,7 +17,6 @@ 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
@@ -33,6 +32,7 @@ from latticeai.runtime.platform_services_runtime import (
33
32
  build_brain_network,
34
33
  build_model_service,
35
34
  )
35
+ from latticeai.runtime.platform_runtime_wiring import build_platform_automation_runtime
36
36
  from latticeai.runtime.persistence_runtime import build_persistence_runtime
37
37
  from latticeai.runtime.router_registration import (
38
38
  build_auth_admin_security_router_bundle,
@@ -45,6 +45,7 @@ from latticeai.runtime.router_registration import (
45
45
  )
46
46
  from latticeai.runtime.security_runtime import build_security_runtime
47
47
  from latticeai.runtime.web_runtime import build_web_runtime
48
+ from latticeai.services.router_context import InteractionRouterContext, ToolRouterContext
48
49
 
49
50
  if TYPE_CHECKING: # imports for annotations only — keep module import light
50
51
  from fastapi import FastAPI
@@ -145,7 +146,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
145
146
  from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
146
147
  from latticeai.api.health import create_health_router
147
148
  # ── v2 Agentic Workspace Platform layers ─────────────────────────────────────
148
- from latticeai.services.platform_runtime import PlatformRuntime
149
149
  from latticeai.api.plugins import create_plugins_router
150
150
  from latticeai.api.workflow_designer import create_workflow_designer_router
151
151
  from latticeai.api.agents import create_agents_router
@@ -189,7 +189,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
189
189
  _get_combined_registry,
190
190
  _fetch_skills_marketplace, install_skill, SKILLS_DIR,
191
191
  )
192
- from p_reinforce import PReinforceGardener
192
+ from latticeai.services.p_reinforce import PReinforceGardener
193
193
  from setup_wizard import get_recommendations, scan_environment
194
194
  from tools import ensure_agent_root, execute_tool, knowledge_save
195
195
 
@@ -1239,7 +1239,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1239
1239
  on_chat_message = None
1240
1240
  if ENABLE_TELEGRAM:
1241
1241
  def _telegram_chat_mirror(role: str, text: str, source: Optional[str] = None) -> None:
1242
- from telegram_bot import broadcast_web_chat
1242
+ from latticeai.integrations.telegram_bot import broadcast_web_chat
1243
1243
  _spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
1244
1244
  on_chat_message = _telegram_chat_mirror
1245
1245
 
@@ -1338,20 +1338,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1338
1338
 
1339
1339
 
1340
1340
  # ── 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,
1341
+ _platform_automation_runtime = build_platform_automation_runtime(
1342
+ model_router=router,
1343
+ workspace_store=WORKSPACE_OS,
1355
1344
  workspace_service=WORKSPACE_SERVICE,
1356
1345
  plugin_registry=PLUGIN_REGISTRY,
1357
1346
  get_current_user=get_current_user,
@@ -1359,23 +1348,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1359
1348
  workspace_scope_from_request=_workspace_scope_from_request,
1360
1349
  get_tool_permission=get_tool_permission,
1361
1350
  hooks=HOOKS_REGISTRY,
1362
- llm_generate=_llm_generate_sync,
1363
- llm_available=lambda: bool(getattr(router, "current_model_id", None)),
1364
1351
  agent_registry=AGENT_REGISTRY,
1365
- )
1366
-
1367
- _automation_runtime = build_automation_runtime(
1368
- store=WORKSPACE_OS,
1369
- platform=PLATFORM,
1370
1352
  data_dir=DATA_DIR,
1371
- workspace_graph=_workspace_graph,
1372
1353
  append_audit_event=append_audit_event,
1373
- hooks=HOOKS_REGISTRY,
1374
1354
  )
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"]
1355
+ _llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
1356
+ PLATFORM = _platform_automation_runtime["PLATFORM"]
1357
+ _automation_runtime = _platform_automation_runtime["_automation_runtime"]
1358
+ REVIEW_QUEUE = _platform_automation_runtime["REVIEW_QUEUE"]
1359
+ TRIGGER_SERVICE = _platform_automation_runtime["TRIGGER_SERVICE"]
1360
+ AGENT_RUNTIME = _platform_automation_runtime["AGENT_RUNTIME"]
1361
+ RUN_EXECUTOR = _platform_automation_runtime["RUN_EXECUTOR"]
1379
1362
  bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
1380
1363
  app.state.run_executor = RUN_EXECUTOR
1381
1364
  app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
@@ -1478,21 +1461,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1478
1461
  return None
1479
1462
  return PLATFORM.allowed_scopes(user)
1480
1463
 
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,
1464
+ tool_router_context = ToolRouterContext(
1492
1465
  config=CONFIG,
1466
+ ingestion_pipeline=INGESTION_PIPELINE,
1493
1467
  data_dir=DATA_DIR,
1494
1468
  static_dir=STATIC_DIR,
1495
1469
  model_router=router,
1470
+ require_user=require_user,
1496
1471
  require_admin=require_admin,
1497
1472
  get_current_user=get_current_user,
1498
1473
  clear_history=clear_history,
@@ -1510,13 +1485,29 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1510
1485
  install_mcp=install_mcp,
1511
1486
  mcp_public_item=mcp_public_item,
1512
1487
  hooks=HOOKS_REGISTRY,
1513
- create_hooks_router=create_hooks_router,
1514
- create_agent_registry_router=create_agent_registry_router,
1488
+ )
1489
+ interaction_router_context = InteractionRouterContext(
1490
+ chat_context=context,
1491
+ search_service=SEARCH_SERVICE,
1492
+ allowed_workspaces_for=_allowed_workspaces_for,
1493
+ require_user=require_user,
1494
+ embedding_info=_embedding_info,
1495
+ tool_context=tool_router_context,
1496
+ hooks=HOOKS_REGISTRY,
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
1522
1513
 
@@ -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.2.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.2.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