ltcai 0.1.9 → 0.1.16

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 (43) hide show
  1. package/README.md +174 -305
  2. package/docs/CHANGELOG.md +307 -0
  3. package/docs/architecture.md +121 -0
  4. package/docs/mcp-tools.md +116 -0
  5. package/docs/privacy.md +74 -0
  6. package/docs/public-deploy.md +137 -0
  7. package/docs/security-model.md +121 -0
  8. package/knowledge_graph.py +123 -15
  9. package/llm_router.py +100 -28
  10. package/ltcai_cli.py +138 -5
  11. package/package.json +14 -2
  12. package/server.py +1756 -329
  13. package/skills/SKILL_TEMPLATE.md +61 -29
  14. package/skills/code_review/SKILL.md +28 -0
  15. package/skills/code_review/examples.md +59 -0
  16. package/skills/code_review/risk.json +9 -0
  17. package/skills/code_review/schema.json +65 -0
  18. package/skills/data_analysis/SKILL.md +28 -0
  19. package/skills/data_analysis/examples.md +62 -0
  20. package/skills/data_analysis/risk.json +9 -0
  21. package/skills/data_analysis/schema.json +61 -0
  22. package/skills/file_edit/SKILL.md +33 -0
  23. package/skills/file_edit/examples.md +45 -0
  24. package/skills/file_edit/risk.json +9 -0
  25. package/skills/file_edit/schema.json +60 -0
  26. package/skills/summarize_document/SKILL.md +68 -0
  27. package/skills/summarize_document/examples.md +65 -0
  28. package/skills/summarize_document/risk.json +9 -0
  29. package/skills/summarize_document/schema.json +71 -0
  30. package/skills/web_search/SKILL.md +28 -0
  31. package/skills/web_search/examples.md +61 -0
  32. package/skills/web_search/risk.json +9 -0
  33. package/skills/web_search/schema.json +62 -0
  34. package/static/account.html +53 -51
  35. package/static/admin.html +50 -46
  36. package/static/chat.html +124 -96
  37. package/static/graph.html +1231 -337
  38. package/static/manifest.json +2 -2
  39. package/tests/integration/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/tests/integration/__pycache__/test_api.cpython-314-pytest-9.0.3.pyc +0 -0
  41. package/tests/unit/__pycache__/test_tools.cpython-314-pytest-9.0.3.pyc +0 -0
  42. package/tests/unit/test_tools.py +194 -1
  43. package/tools.py +264 -4
package/ltcai_cli.py CHANGED
@@ -5,9 +5,16 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import importlib.util
7
7
  import os
8
+ import platform
9
+ import re
8
10
  import shutil
9
11
  import socket
12
+ import stat
13
+ import subprocess
10
14
  import sys
15
+ import threading
16
+ import time
17
+ import urllib.request
11
18
  from pathlib import Path
12
19
 
13
20
 
@@ -16,7 +23,6 @@ def _has_module(name: str) -> bool:
16
23
 
17
24
 
18
25
  def _local_ips() -> list[str]:
19
- """Return all non-loopback IPv4 addresses for this machine."""
20
26
  ips: list[str] = []
21
27
  try:
22
28
  hostname = socket.gethostname()
@@ -38,7 +44,7 @@ def _local_ips() -> list[str]:
38
44
  return ips
39
45
 
40
46
 
41
- def _print_banner(host: str, port: int) -> None:
47
+ def _print_banner(host: str, port: int, tunnel_url: str | None = None) -> None:
42
48
  local_url = f"http://localhost:{port}"
43
49
  print()
44
50
  print("=" * 56)
@@ -51,6 +57,10 @@ def _print_banner(host: str, port: int) -> None:
51
57
  print(" Other devices on the same Wi-Fi can open the")
52
58
  print(" Network URL above in their browser.")
53
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.")
54
64
  print("=" * 56)
55
65
  print()
56
66
 
@@ -85,14 +95,122 @@ def doctor() -> int:
85
95
  return 0 if ok else 1
86
96
 
87
97
 
98
+ # ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
99
+
100
+ def _cloudflared_url() -> str:
101
+ system = sys.platform
102
+ machine = platform.machine().lower()
103
+ base = "https://github.com/cloudflare/cloudflared/releases/latest/download"
104
+ if system == "darwin":
105
+ arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
106
+ return f"{base}/cloudflared-darwin-{arch}"
107
+ if system == "win32":
108
+ return f"{base}/cloudflared-windows-amd64.exe"
109
+ arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
110
+ return f"{base}/cloudflared-linux-{arch}"
111
+
112
+
113
+ def _cloudflared_bin() -> Path:
114
+ suffix = ".exe" if sys.platform == "win32" else ""
115
+ return Path.home() / ".latticeai" / "bin" / f"cloudflared{suffix}"
116
+
117
+
118
+ def _ensure_cloudflared() -> str:
119
+ found = shutil.which("cloudflared")
120
+ if found:
121
+ return found
122
+ dest = _cloudflared_bin()
123
+ if dest.exists():
124
+ return str(dest)
125
+ dest.parent.mkdir(parents=True, exist_ok=True)
126
+ url = _cloudflared_url()
127
+ print(f" cloudflared not found — downloading from GitHub...")
128
+ try:
129
+ urllib.request.urlretrieve(url, dest)
130
+ if sys.platform != "win32":
131
+ dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
132
+ print(f" cloudflared installed at {dest}")
133
+ return str(dest)
134
+ except Exception as e:
135
+ print(f" cloudflared download failed: {e}")
136
+ print(" Install manually: https://developers.cloudflare.com/cloudflared/install")
137
+ return ""
138
+
139
+
140
+ def _send_telegram(token: str, chat_id: str, text: str) -> None:
141
+ try:
142
+ data = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode()
143
+ req = urllib.request.Request(
144
+ f"https://api.telegram.org/bot{token}/sendMessage",
145
+ data=data,
146
+ )
147
+ urllib.request.urlopen(req, timeout=10)
148
+ except Exception:
149
+ pass
150
+
151
+
152
+ def _start_tunnel(port: int) -> str | None:
153
+ import urllib.parse
154
+
155
+ bin_path = _ensure_cloudflared()
156
+ if not bin_path:
157
+ return None
158
+
159
+ log_path = Path.home() / ".latticeai" / "tunnel.log"
160
+ log_path.parent.mkdir(parents=True, exist_ok=True)
161
+
162
+ proc = subprocess.Popen(
163
+ [bin_path, "tunnel", "--url", f"http://localhost:{port}"],
164
+ stdout=open(log_path, "w"),
165
+ stderr=subprocess.STDOUT,
166
+ )
167
+
168
+ # Wait for the public URL (up to 30s)
169
+ pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
170
+ deadline = time.time() + 30
171
+ url: str | None = None
172
+ while time.time() < deadline:
173
+ time.sleep(0.5)
174
+ try:
175
+ text = log_path.read_text(errors="replace")
176
+ m = pattern.search(text)
177
+ if m:
178
+ url = m.group(0)
179
+ break
180
+ except Exception:
181
+ pass
182
+
183
+ if not url:
184
+ return None
185
+
186
+ # Telegram notification if configured
187
+ token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
188
+ chat_id = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
189
+ if token and chat_id:
190
+ msg = (
191
+ f"✅ Lattice AI 시작됨\n\n"
192
+ f"🌐 외부 URL: {url}\n"
193
+ f"🏠 로컬: http://localhost:{port}"
194
+ )
195
+ threading.Thread(target=_send_telegram, args=(token, chat_id, msg), daemon=True).start()
196
+
197
+ return url
198
+
199
+
200
+ # ─────────────────────────────────────────────────────────────────────────────
201
+
88
202
  def main() -> None:
89
203
  parser = argparse.ArgumentParser(prog="LTCAI", description="Run the Lattice AI local server.")
90
204
  subparsers = parser.add_subparsers(dest="command")
91
205
  subparsers.add_parser("doctor", help="Check local runtime dependencies and configuration.")
92
- # Default to 0.0.0.0 so other devices on the same network can connect
93
- parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "0.0.0.0")
206
+ parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "127.0.0.1")
94
207
  parser.add_argument("--port", type=int, default=int(os.getenv("LATTICEAI_PORT") or "4825"))
95
208
  parser.add_argument("--reload", action="store_true", help="Enable uvicorn reload for local development.")
209
+ parser.add_argument(
210
+ "--tunnel",
211
+ action="store_true",
212
+ help="Open a public Cloudflare tunnel so anyone can access this server from the internet.",
213
+ )
96
214
  args = parser.parse_args()
97
215
 
98
216
  if args.command == "doctor":
@@ -101,7 +219,22 @@ def main() -> None:
101
219
  app_dir = Path(__file__).resolve().parent
102
220
  os.chdir(app_dir)
103
221
 
104
- _print_banner(args.host, args.port)
222
+ # --tunnel forces 0.0.0.0 so cloudflared can reach the server
223
+ if args.tunnel and args.host == "127.0.0.1":
224
+ args.host = "0.0.0.0"
225
+ os.environ.setdefault("LATTICEAI_HOST", "0.0.0.0")
226
+ os.environ.setdefault("LATTICEAI_CORS_ALLOW_NETWORK", "true")
227
+ os.environ.setdefault("LATTICEAI_REQUIRE_AUTH", "true")
228
+
229
+ tunnel_url: str | None = None
230
+ if args.tunnel:
231
+ print()
232
+ print(" Starting Cloudflare tunnel...")
233
+ tunnel_url = _start_tunnel(args.port)
234
+ if not tunnel_url:
235
+ print(" ⚠️ Tunnel URL not obtained — server will start without tunnel.")
236
+
237
+ _print_banner(args.host, args.port, tunnel_url)
105
238
 
106
239
  import uvicorn
107
240
 
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "0.1.9",
3
+ "version": "0.1.16",
4
4
  "description": "Lattice AI local MLX/cloud LLM workspace server",
5
+ "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/TaeSooPark-PTS/LatticeAI.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/TaeSooPark-PTS/LatticeAI/issues"
12
+ },
5
13
  "bin": {
6
14
  "ltcai": "bin/ltcai.js",
7
15
  "LTCAI": "bin/ltcai.js"
@@ -19,10 +27,14 @@
19
27
  },
20
28
  "keywords": [
21
29
  "ltcai",
30
+ "ai-assistant",
22
31
  "llm",
23
32
  "mlx",
24
33
  "local-ai",
25
- "agent"
34
+ "agent",
35
+ "mcp",
36
+ "rag",
37
+ "vscode"
26
38
  ],
27
39
  "license": "MIT",
28
40
  "private": false,