ltcai 6.0.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 (54) hide show
  1. package/README.md +33 -35
  2. package/docs/CHANGELOG.md +68 -0
  3. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +1 -1
  4. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +1 -1
  5. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +1 -1
  6. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +1 -1
  7. package/frontend/src/App.tsx +3 -1281
  8. package/frontend/src/components/LanguageSwitcher.tsx +23 -0
  9. package/frontend/src/components/ProductFlow.tsx +32 -662
  10. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +688 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  12. package/frontend/src/features/brain/BrainHome.tsx +999 -0
  13. package/frontend/src/features/brain/brainData.ts +98 -0
  14. package/frontend/src/features/brain/graphLayout.ts +26 -0
  15. package/frontend/src/features/brain/types.ts +44 -0
  16. package/frontend/src/features/review/ReviewCard.tsx +15 -10
  17. package/frontend/src/i18n.ts +208 -0
  18. package/frontend/src/styles.css +240 -0
  19. package/lattice_brain/__init__.py +1 -1
  20. package/lattice_brain/runtime/multi_agent.py +1 -1
  21. package/latticeai/__init__.py +1 -1
  22. package/latticeai/api/chat.py +52 -33
  23. package/latticeai/api/tools.py +50 -23
  24. package/latticeai/app_factory.py +65 -47
  25. package/latticeai/cli/__init__.py +1 -0
  26. package/latticeai/cli/entrypoint.py +283 -0
  27. package/latticeai/cli/runtime.py +37 -0
  28. package/latticeai/core/marketplace.py +1 -1
  29. package/latticeai/core/workspace_os.py +1 -1
  30. package/latticeai/integrations/__init__.py +0 -0
  31. package/latticeai/integrations/telegram_bot.py +1009 -0
  32. package/latticeai/runtime/lifespan_runtime.py +1 -1
  33. package/latticeai/runtime/platform_runtime_wiring.py +87 -0
  34. package/latticeai/runtime/router_registration.py +49 -30
  35. package/latticeai/services/app_context.py +1 -0
  36. package/latticeai/services/p_reinforce.py +258 -0
  37. package/latticeai/services/router_context.py +52 -0
  38. package/latticeai/services/tool_dispatch.py +82 -25
  39. package/ltcai_cli.py +7 -305
  40. package/p_reinforce.py +4 -255
  41. package/package.json +2 -1
  42. package/scripts/release_smoke.py +133 -0
  43. package/scripts/wheel_smoke.py +4 -0
  44. package/src-tauri/Cargo.lock +1 -1
  45. package/src-tauri/Cargo.toml +1 -1
  46. package/src-tauri/tauri.conf.json +1 -1
  47. package/static/app/asset-manifest.json +5 -5
  48. package/static/app/assets/{index-xRn29gI8.css → index-B2-1Gm0q.css} +1 -1
  49. package/static/app/assets/index-D91Rz5--.js +16 -0
  50. package/static/app/assets/index-D91Rz5--.js.map +1 -0
  51. package/static/app/index.html +2 -2
  52. package/telegram_bot.py +9 -1008
  53. package/static/app/assets/index-D2zafMYb.js +0 -16
  54. package/static/app/assets/index-D2zafMYb.js.map +0 -1
@@ -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
@@ -153,7 +153,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
153
153
  from latticeai.api.invitations import create_invitations_router
154
154
  from latticeai.api.marketplace import create_marketplace_router
155
155
  from latticeai.api.models import create_models_router
156
- from latticeai.api.chat import create_chat_router
156
+ from latticeai.api.chat import build_recent_chat_context, create_chat_router
157
157
  from latticeai.api.search import create_search_router
158
158
  from latticeai.api.tools import create_tools_router
159
159
  from latticeai.api.static_routes import create_static_routes_router
@@ -181,6 +181,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
181
181
  configure_tool_dispatch,
182
182
  get_tool_permission,
183
183
  list_tool_permissions,
184
+ build_agent_runtime,
184
185
  tool_response as _tool_response,
185
186
  )
186
187
  from latticeai.core.tool_registry import TOOL_CATALOG_BRIEF as _TOOL_CATALOG_BRIEF # noqa: F401
@@ -188,9 +189,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
188
189
  _get_combined_registry,
189
190
  _fetch_skills_marketplace, install_skill, SKILLS_DIR,
190
191
  )
191
- from p_reinforce import PReinforceGardener
192
+ from latticeai.services.p_reinforce import PReinforceGardener
192
193
  from setup_wizard import get_recommendations, scan_environment
193
- from tools import ensure_agent_root
194
+ from tools import ensure_agent_root, execute_tool, knowledge_save
194
195
 
195
196
  try:
196
197
  import keyring
@@ -1238,10 +1239,35 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1238
1239
  on_chat_message = None
1239
1240
  if ENABLE_TELEGRAM:
1240
1241
  def _telegram_chat_mirror(role: str, text: str, source: Optional[str] = None) -> None:
1241
- from telegram_bot import broadcast_web_chat
1242
+ from latticeai.integrations.telegram_bot import broadcast_web_chat
1242
1243
  _spawn(broadcast_web_chat(role, text), name="telegram_broadcast")
1243
1244
  on_chat_message = _telegram_chat_mirror
1244
1245
 
1246
+ def _recent_chat_context(
1247
+ limit: int = 10,
1248
+ include_image_missing_replies: bool = True,
1249
+ user_email: Optional[str] = None,
1250
+ conversation_id: Optional[str] = None,
1251
+ ) -> str:
1252
+ return build_recent_chat_context(
1253
+ get_history=get_history,
1254
+ limit=limit,
1255
+ include_image_missing_replies=include_image_missing_replies,
1256
+ user_email=user_email,
1257
+ conversation_id=conversation_id,
1258
+ )
1259
+
1260
+ CHAT_AGENT_RUNTIME = build_agent_runtime(
1261
+ model_router=router,
1262
+ execute_tool=execute_tool,
1263
+ recent_chat_context=_recent_chat_context,
1264
+ clear_history=clear_history,
1265
+ knowledge_save=knowledge_save,
1266
+ audit=append_audit_event,
1267
+ hooks=HOOKS_REGISTRY,
1268
+ brain_memory=BRAIN_MEMORY,
1269
+ )
1270
+
1245
1271
  # ── Typed dependency context (latticeai.services.app_context) ────────────────
1246
1272
  # One context object replaces the historical 25-30-kwarg router wiring.
1247
1273
  context = build_app_context(
@@ -1258,6 +1284,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1258
1284
  chat_service=CHAT_SERVICE,
1259
1285
  context_assembler=CONTEXT_ASSEMBLER,
1260
1286
  brain_memory=BRAIN_MEMORY,
1287
+ chat_agent_runtime=CHAT_AGENT_RUNTIME,
1261
1288
  gardener=gardener,
1262
1289
  hooks=HOOKS_REGISTRY,
1263
1290
  realtime_bus=REALTIME_BUS,
@@ -1311,20 +1338,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1311
1338
 
1312
1339
 
1313
1340
  # ── v2 Agentic Workspace Platform: cross-system wiring ───────────────────────
1314
- # All cross-subsystem closures live in latticeai.services.platform_runtime to
1315
- # keep this assembly file lean; server_app only constructs it and mounts routers.
1316
- def _llm_generate_sync(message: str, context: str = "", max_tokens: int = 1024, temperature: float = 0.1) -> str:
1317
- # Synchronous model bridge for the orchestrator's role runner. Safe
1318
- # because the agents run endpoint executes start() in a worker thread
1319
- # (asyncio.to_thread), where no event loop is running.
1320
- import asyncio as _asyncio
1321
-
1322
- return str(_asyncio.run(router.generate(
1323
- message, context=context, max_tokens=max_tokens, temperature=temperature,
1324
- )))
1325
-
1326
- PLATFORM = PlatformRuntime(
1327
- store=WORKSPACE_OS,
1341
+ _platform_automation_runtime = build_platform_automation_runtime(
1342
+ model_router=router,
1343
+ workspace_store=WORKSPACE_OS,
1328
1344
  workspace_service=WORKSPACE_SERVICE,
1329
1345
  plugin_registry=PLUGIN_REGISTRY,
1330
1346
  get_current_user=get_current_user,
@@ -1332,23 +1348,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1332
1348
  workspace_scope_from_request=_workspace_scope_from_request,
1333
1349
  get_tool_permission=get_tool_permission,
1334
1350
  hooks=HOOKS_REGISTRY,
1335
- llm_generate=_llm_generate_sync,
1336
- llm_available=lambda: bool(getattr(router, "current_model_id", None)),
1337
1351
  agent_registry=AGENT_REGISTRY,
1338
- )
1339
-
1340
- _automation_runtime = build_automation_runtime(
1341
- store=WORKSPACE_OS,
1342
- platform=PLATFORM,
1343
1352
  data_dir=DATA_DIR,
1344
- workspace_graph=_workspace_graph,
1345
1353
  append_audit_event=append_audit_event,
1346
- hooks=HOOKS_REGISTRY,
1347
1354
  )
1348
- REVIEW_QUEUE = _automation_runtime["REVIEW_QUEUE"]
1349
- TRIGGER_SERVICE = _automation_runtime["TRIGGER_SERVICE"]
1350
- AGENT_RUNTIME = _automation_runtime["AGENT_RUNTIME"]
1351
- 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"]
1352
1362
  bind_trigger_hook_runner(registry=HOOKS_REGISTRY, trigger_service=TRIGGER_SERVICE)
1353
1363
  app.state.run_executor = RUN_EXECUTOR
1354
1364
  app.state.run_reconciliation = RUN_EXECUTOR.reconcile_startup()
@@ -1451,21 +1461,13 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1451
1461
  return None
1452
1462
  return PLATFORM.allowed_scopes(user)
1453
1463
 
1454
- register_interaction_routers(
1455
- app,
1456
- create_chat_router=create_chat_router,
1457
- context=context,
1458
- create_search_router=create_search_router,
1459
- search_service=SEARCH_SERVICE,
1460
- allowed_workspaces_for=_allowed_workspaces_for,
1461
- require_user=require_user,
1462
- embedding_info=_embedding_info,
1463
- create_tools_router=create_tools_router,
1464
- ingestion_pipeline=INGESTION_PIPELINE,
1464
+ tool_router_context = ToolRouterContext(
1465
1465
  config=CONFIG,
1466
+ ingestion_pipeline=INGESTION_PIPELINE,
1466
1467
  data_dir=DATA_DIR,
1467
1468
  static_dir=STATIC_DIR,
1468
1469
  model_router=router,
1470
+ require_user=require_user,
1469
1471
  require_admin=require_admin,
1470
1472
  get_current_user=get_current_user,
1471
1473
  clear_history=clear_history,
@@ -1483,13 +1485,29 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1483
1485
  install_mcp=install_mcp,
1484
1486
  mcp_public_item=mcp_public_item,
1485
1487
  hooks=HOOKS_REGISTRY,
1486
- create_hooks_router=create_hooks_router,
1487
- 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,
1488
1497
  agent_registry=AGENT_REGISTRY,
1489
- create_memory_router=create_memory_router,
1490
1498
  memory_service=MEMORY_SERVICE,
1491
1499
  platform=PLATFORM,
1492
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
+ )
1493
1511
 
1494
1512
  from latticeai.api.review_queue import create_review_queue_router
1495
1513
 
@@ -0,0 +1 @@
1
+ """LatticeAI CLI package."""
@@ -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()
@@ -0,0 +1,37 @@
1
+ """Pure runtime helpers extracted from ltcai_cli entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ def _load_env_file(path: Path) -> None:
11
+ if not path.exists():
12
+ return
13
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
14
+ line = raw_line.strip()
15
+ if not line or line.startswith("#") or "=" not in line:
16
+ continue
17
+ key, value = line.split("=", 1)
18
+ key = key.strip()
19
+ value = value.strip().strip('"').strip("'")
20
+ if key and key not in os.environ:
21
+ os.environ[key] = value
22
+
23
+
24
+ def _apply_extra_path() -> None:
25
+ extra = os.getenv("LATTICEAI_EXTRA_PATH", "")
26
+ if not extra:
27
+ return
28
+ current = [p for p in os.environ.get("PATH", "").split(os.pathsep) if p]
29
+ for item in reversed([p for p in extra.split(os.pathsep) if p]):
30
+ expanded = str(Path(item).expanduser())
31
+ if Path(expanded).exists() and expanded not in current:
32
+ current.insert(0, expanded)
33
+ os.environ["PATH"] = os.pathsep.join(current)
34
+
35
+
36
+ def _has_module(name: str) -> bool:
37
+ return importlib.util.find_spec(name) is not None
@@ -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.0.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.0.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