ltcai 8.9.0 → 9.1.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 (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
@@ -1,345 +1,7 @@
1
- """Local folder knowledge-source API and optional filesystem watcher."""
1
+ """Compatibility shim for :mod:`latticeai.services.local_knowledge`."""
2
2
 
3
- import logging
4
- import threading
5
- from pathlib import Path
6
- from typing import Any, Callable, Dict, Optional
3
+ import sys
7
4
 
8
- from fastapi import APIRouter, HTTPException, Request
9
- from pydantic import BaseModel, Field
5
+ from latticeai.services import local_knowledge as _impl
10
6
 
11
-
12
- class LocalTreeRequest(BaseModel):
13
- path: str
14
- max_items: int = 200
15
- approved: bool = False
16
- approval_token: Optional[str] = None
17
-
18
-
19
- class LocalKnowledgeAuditRequest(BaseModel):
20
- path: str
21
- include_ocr: bool = False
22
- max_files: int = 50_000
23
- approved: bool = False
24
- approval_token: Optional[str] = None
25
-
26
-
27
- class LocalKnowledgeIndexRequest(BaseModel):
28
- path: str
29
- include_ocr: bool = False
30
- watch_enabled: bool = False
31
- max_files: int = 5_000
32
- consent: Dict[str, Any] = Field(default_factory=dict)
33
- approved: bool = False
34
- approval_token: Optional[str] = None
35
-
36
-
37
- class LocalKnowledgeWatchRequest(BaseModel):
38
- source_id: str
39
-
40
-
41
- class _LocalWatchHandler:
42
- def __init__(self, schedule_change: Callable[[], None]):
43
- self._schedule_change = schedule_change
44
-
45
- def on_any_event(self, event): # pragma: no cover - exercised by OS watcher
46
- if getattr(event, "is_directory", False):
47
- return
48
- self._schedule_change()
49
-
50
-
51
- class LocalKnowledgeWatcher:
52
- """Debounced watchdog wrapper for approved local knowledge sources."""
53
-
54
- def __init__(self, get_graph: Callable[[], Any], *, debounce_seconds: float = 5.0, hooks: Any = None):
55
- self._get_graph = get_graph
56
- self._debounce_seconds = debounce_seconds
57
- self._hooks = hooks
58
- self._lock = threading.Lock()
59
- self._watched: Dict[str, Dict[str, Any]] = {}
60
- self._observer_cls = None
61
- self._event_handler_base = None
62
- self._import_error = ""
63
- try:
64
- from watchdog.events import FileSystemEventHandler
65
- from watchdog.observers import Observer
66
-
67
- self._observer_cls = Observer
68
- self._event_handler_base = FileSystemEventHandler
69
- except Exception as exc: # pragma: no cover - depends on optional dependency
70
- self._import_error = str(exc)
71
-
72
- @property
73
- def available(self) -> bool:
74
- return self._observer_cls is not None and self._event_handler_base is not None
75
-
76
- def status(self) -> Dict[str, Any]:
77
- with self._lock:
78
- active = {
79
- source_id: {
80
- "root_path": item["source"].get("root_path"),
81
- "last_event_at": item.get("last_event_at"),
82
- "last_indexed_at": item.get("last_indexed_at"),
83
- "last_error": item.get("last_error"),
84
- }
85
- for source_id, item in self._watched.items()
86
- }
87
- return {
88
- "available": self.available,
89
- "error": "" if self.available else self._import_error or "watchdog is not installed",
90
- "debounce_seconds": self._debounce_seconds,
91
- "active": active,
92
- }
93
-
94
- def restore_enabled_sources(self) -> Dict[str, Any]:
95
- graph = self._get_graph()
96
- if graph is None:
97
- return {"restored": 0, "available": self.available}
98
- restored = 0
99
- try:
100
- for source in graph.local_sources().get("sources", []):
101
- if source.get("watch_enabled"):
102
- result = self.start_source(source)
103
- if result.get("watching"):
104
- restored += 1
105
- except Exception as exc:
106
- logging.warning("local knowledge watcher restore failed: %s", exc)
107
- return {"restored": restored, "available": self.available}
108
-
109
- def start_source(self, source: Dict[str, Any]) -> Dict[str, Any]:
110
- source_id = str(source.get("id") or "")
111
- root_path = str(source.get("root_path") or "")
112
- if not source_id or not root_path:
113
- return {"watching": False, "error": "source_id and root_path are required"}
114
- if not self.available:
115
- return {"watching": False, "source_id": source_id, "error": self._import_error or "watchdog is not installed"}
116
- root = Path(root_path).expanduser().resolve()
117
- if not root.exists() or not root.is_dir():
118
- return {"watching": False, "source_id": source_id, "error": "source folder is not available"}
119
-
120
- self.stop_source(source_id)
121
-
122
- class Handler(_LocalWatchHandler, self._event_handler_base): # type: ignore[misc, valid-type]
123
- def __init__(handler_self):
124
- self._event_handler_base.__init__(handler_self)
125
- _LocalWatchHandler.__init__(handler_self, lambda: self._schedule(source_id))
126
-
127
- observer = self._observer_cls()
128
- try:
129
- observer.schedule(Handler(), str(root), recursive=True)
130
- observer.start()
131
- except Exception as exc:
132
- logging.warning("local knowledge watcher start failed for %s: %s", root, exc)
133
- return {"watching": False, "source_id": source_id, "error": str(exc)}
134
-
135
- with self._lock:
136
- self._watched[source_id] = {
137
- "observer": observer,
138
- "timer": None,
139
- "source": dict(source),
140
- "last_event_at": None,
141
- "last_indexed_at": None,
142
- "last_error": None,
143
- }
144
- return {"watching": True, "source_id": source_id, "root_path": str(root)}
145
-
146
- def stop_source(self, source_id: str) -> Dict[str, Any]:
147
- with self._lock:
148
- item = self._watched.pop(source_id, None)
149
- if not item:
150
- return {"stopped": False, "source_id": source_id}
151
- timer = item.get("timer")
152
- if timer:
153
- timer.cancel()
154
- observer = item.get("observer")
155
- try:
156
- observer.stop()
157
- observer.join(timeout=3)
158
- except Exception as exc:
159
- logging.warning("local knowledge watcher stop failed for %s: %s", source_id, exc)
160
- return {"stopped": True, "source_id": source_id}
161
-
162
- def stop_all(self) -> None:
163
- for source_id in list(self.status().get("active", {}).keys()):
164
- self.stop_source(source_id)
165
-
166
- def _schedule(self, source_id: str) -> None:
167
- with self._lock:
168
- item = self._watched.get(source_id)
169
- if not item:
170
- return
171
- timer = item.get("timer")
172
- if timer:
173
- timer.cancel()
174
- item["last_event_at"] = _now_seconds()
175
- timer = threading.Timer(self._debounce_seconds, self._run_index, args=(source_id,))
176
- timer.daemon = True
177
- item["timer"] = timer
178
- timer.start()
179
-
180
- def _run_index(self, source_id: str) -> None:
181
- with self._lock:
182
- item = self._watched.get(source_id)
183
- if not item:
184
- return
185
- source = dict(item["source"])
186
- item["timer"] = None
187
- graph = self._get_graph()
188
- if graph is None:
189
- return
190
- consent = source.get("consent") or {}
191
- root = source.get("root_path")
192
- if self._hooks is not None:
193
- self._hooks.fire_hook("pre_index", "folder.reindex",
194
- payload={"source_id": source_id, "root_path": root, "trigger": "watch"})
195
- try:
196
- graph.index_local_folder(
197
- Path(source["root_path"]),
198
- include_ocr=bool(source.get("include_ocr")),
199
- watch_enabled=True,
200
- user_email=consent.get("approved_by"),
201
- consent=consent,
202
- )
203
- with self._lock:
204
- if source_id in self._watched:
205
- self._watched[source_id]["last_indexed_at"] = _now_seconds()
206
- self._watched[source_id]["last_error"] = None
207
- if self._hooks is not None:
208
- self._hooks.fire_hook("post_index", "folder.reindex",
209
- payload={"source_id": source_id, "root_path": root, "trigger": "watch", "status": "ok"})
210
- except Exception as exc:
211
- logging.warning("local knowledge watcher reindex failed for %s: %s", source_id, exc)
212
- with self._lock:
213
- if source_id in self._watched:
214
- self._watched[source_id]["last_error"] = str(exc)
215
- if self._hooks is not None:
216
- self._hooks.fire_hook("post_index", "folder.reindex",
217
- payload={"source_id": source_id, "root_path": root, "trigger": "watch", "status": "error", "error": str(exc)})
218
-
219
-
220
- def _now_seconds() -> float:
221
- import time
222
-
223
- return time.time()
224
-
225
-
226
- def create_local_knowledge_router(
227
- *,
228
- get_graph: Callable[[], Any],
229
- require_graph: Callable[[], None],
230
- require_user: Callable[[Request], str],
231
- require_local_user: Callable[[Request], str],
232
- local_permission_response: Callable[..., dict],
233
- require_local_approval: Callable[..., None],
234
- watcher: Optional[LocalKnowledgeWatcher] = None,
235
- hooks: Any = None,
236
- ) -> APIRouter:
237
- router = APIRouter()
238
-
239
- def graph():
240
- require_graph()
241
- return get_graph()
242
-
243
- @router.get("/knowledge-graph/local/roots")
244
- async def knowledge_graph_local_roots(request: Request):
245
- require_user(request)
246
- return graph().discover_local_roots()
247
-
248
- @router.get("/knowledge-graph/local/sources")
249
- async def knowledge_graph_local_sources(request: Request):
250
- require_user(request)
251
- payload = graph().local_sources()
252
- watch_status = watcher.status() if watcher else {"available": False, "active": {}}
253
- active = watch_status.get("active", {})
254
- for source in payload.get("sources", []):
255
- source["watch_active"] = source.get("id") in active
256
- source["watch_status"] = active.get(source.get("id"))
257
- payload["watch"] = watch_status
258
- return payload
259
-
260
- @router.get("/knowledge-graph/local/watch/status")
261
- async def knowledge_graph_local_watch_status(request: Request):
262
- require_user(request)
263
- graph()
264
- return watcher.status() if watcher else {"available": False, "active": {}, "error": "watcher unavailable"}
265
-
266
- @router.post("/knowledge-graph/local/watch/stop")
267
- async def knowledge_graph_local_watch_stop(req: LocalKnowledgeWatchRequest, request: Request):
268
- require_user(request)
269
- kg = graph()
270
- try:
271
- kg.set_local_source_watch(req.source_id, False)
272
- except ValueError as exc:
273
- raise HTTPException(status_code=404, detail=str(exc)) from exc
274
- result = watcher.stop_source(req.source_id) if watcher else {"stopped": False, "source_id": req.source_id}
275
- return {"status": "ok", "watch": result}
276
-
277
- @router.post("/knowledge-graph/local/tree")
278
- async def knowledge_graph_local_tree(req: LocalTreeRequest, request: Request):
279
- current_user = require_local_user(request)
280
- kg = graph()
281
- if not req.approved:
282
- return local_permission_response(req.path, "list", current_user)
283
- require_local_approval(token=req.approval_token, path=req.path, action="list", user_email=current_user)
284
- try:
285
- return kg.preview_local_tree(Path(req.path), max_items=req.max_items)
286
- except ValueError as exc:
287
- raise HTTPException(status_code=400, detail=str(exc)) from exc
288
-
289
- @router.post("/knowledge-graph/local/audit")
290
- async def knowledge_graph_local_audit(req: LocalKnowledgeAuditRequest, request: Request):
291
- current_user = require_local_user(request)
292
- kg = graph()
293
- if not req.approved:
294
- return local_permission_response(req.path, "list", current_user)
295
- require_local_approval(token=req.approval_token, path=req.path, action="list", user_email=current_user)
296
- try:
297
- return kg.audit_local_folder(Path(req.path), include_ocr=req.include_ocr, max_files=req.max_files)
298
- except ValueError as exc:
299
- raise HTTPException(status_code=400, detail=str(exc)) from exc
300
-
301
- @router.post("/knowledge-graph/local/index")
302
- async def knowledge_graph_local_index(req: LocalKnowledgeIndexRequest, request: Request):
303
- current_user = require_local_user(request)
304
- kg = graph()
305
- if not req.approved:
306
- return local_permission_response(req.path, "read", current_user)
307
- require_local_approval(token=req.approval_token, path=req.path, action="read", user_email=current_user)
308
- if hooks is not None:
309
- hooks.fire_hook("pre_index", "folder.index",
310
- payload={"root_path": req.path, "trigger": "connect", "watch": req.watch_enabled},
311
- user_email=current_user)
312
- try:
313
- result = kg.index_local_folder(
314
- Path(req.path),
315
- include_ocr=req.include_ocr,
316
- watch_enabled=req.watch_enabled,
317
- user_email=current_user,
318
- consent=req.consent or {},
319
- max_files=req.max_files,
320
- )
321
- except ValueError as exc:
322
- if hooks is not None:
323
- hooks.fire_hook("post_index", "folder.index",
324
- payload={"root_path": req.path, "trigger": "connect", "status": "error", "error": str(exc)},
325
- user_email=current_user)
326
- raise HTTPException(status_code=400, detail=str(exc)) from exc
327
- if hooks is not None:
328
- _idx = (result.get("index") or {}) if isinstance(result, dict) else {}
329
- hooks.fire_hook("post_index", "folder.index",
330
- payload={"root_path": req.path, "trigger": "connect", "status": "ok",
331
- "indexed": _idx.get("indexed") or (result or {}).get("indexed")},
332
- user_email=current_user)
333
-
334
- if watcher:
335
- if req.watch_enabled:
336
- source_payload = {
337
- **result.get("source", {}),
338
- "consent": {"approved_by": current_user, **(req.consent or {})},
339
- }
340
- result["watch"] = watcher.start_source(source_payload)
341
- else:
342
- result["watch"] = watcher.stop_source(result.get("source", {}).get("id", ""))
343
- return result
344
-
345
- return router
7
+ sys.modules[__name__] = _impl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "8.9.0",
3
+ "version": "9.1.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -20,18 +20,23 @@
20
20
  "build": "npm run build:assets && npm run build:python",
21
21
  "frontend:dev": "vite --host 127.0.0.1",
22
22
  "frontend:openapi": "node scripts/run_python.mjs scripts/export_openapi.py frontend/openapi.json && npx openapi-typescript frontend/openapi.json -o frontend/src/api/openapi.ts",
23
+ "frontend:openapi:check": "node scripts/check_openapi_drift.mjs",
23
24
  "build:assets": "vite build && node scripts/build_frontend_assets.mjs",
24
25
  "build:python": "node scripts/run_python.mjs -m build",
25
26
  "check:python": "node scripts/run_python.mjs scripts/check_python.py",
26
- "lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && node scripts/check_i18n_literals.mjs",
27
+ "lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run test:browser-extension",
27
28
  "lint:python": "node scripts/run_python.mjs -m ruff check .",
28
29
  "lint:frontend": "node scripts/lint_frontend.mjs",
29
30
  "docs:check-links": "node scripts/check_markdown_links.mjs",
31
+ "docs:check-current": "node scripts/check_current_release_docs.mjs",
30
32
  "typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
31
33
  "typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
32
34
  "test": "node scripts/run_python.mjs -m pytest tests/ -v",
35
+ "test:frontend": "vitest run",
36
+ "test:frontend:watch": "vitest",
33
37
  "test:unit": "node scripts/run_python.mjs -m pytest tests/unit/ -v",
34
38
  "test:integration": "node scripts/run_integration_tests.mjs",
39
+ "test:browser-extension": "node --check browser-extension/popup.js && node --check browser-extension/capture-page.js && node --test tests/browser-extension.test.cjs",
35
40
  "test:visual": "playwright test",
36
41
  "vercel:build": "node scripts/build_vercel_static.mjs",
37
42
  "desktop:tauri": "tauri dev",
@@ -39,6 +44,7 @@
39
44
  "desktop:tauri:check": "cd src-tauri && cargo check",
40
45
  "desktop:electron": "electron desktop/electron/main.cjs",
41
46
  "package:vsix": "node scripts/build_vsix.mjs",
47
+ "release:evidence": "node scripts/capture_release_evidence.mjs",
42
48
  "release:artifacts": "node scripts/clean_release_artifacts.mjs $npm_package_version && npm run build:assets && npm run build:python && npm pack && npm run package:vsix && npm run desktop:tauri:build",
43
49
  "release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz --require-dmg",
44
50
  "release:smoke": "node scripts/run_python.mjs scripts/release_smoke.py $npm_package_version",
@@ -90,7 +96,16 @@
90
96
  "static/icons/",
91
97
  "plugins/",
92
98
  "scripts/",
99
+ "!bin/pts-grok",
93
100
  "!scripts/launch-pts-grok.sh",
101
+ "!scripts/*discord-bridge*.mjs",
102
+ "!scripts/start-*-discord.sh",
103
+ "!scripts/com.*.discord.plist",
104
+ "!HEARTBEAT.md",
105
+ "!IDENTITY.md",
106
+ "!SOUL.md",
107
+ "!TOOLS.md",
108
+ "!USER.md",
94
109
  "!docs/images/tmp_frames/",
95
110
  "!**/__pycache__/",
96
111
  "!**/*.pyc",
@@ -118,6 +133,9 @@
118
133
  "@tanstack/react-query": "^5.101.0",
119
134
  "@tauri-apps/api": "^2.11.1",
120
135
  "@tauri-apps/cli": "^2.11.3",
136
+ "@testing-library/jest-dom": "^6.9.1",
137
+ "@testing-library/react": "^16.3.2",
138
+ "@testing-library/user-event": "^14.6.1",
121
139
  "@types/cytoscape": "^3.21.9",
122
140
  "@types/react": "^19.2.17",
123
141
  "@types/react-dom": "^19.2.3",
@@ -127,6 +145,7 @@
127
145
  "clsx": "^2.1.1",
128
146
  "cytoscape": "^3.34.0",
129
147
  "electron": "^42.4.0",
148
+ "jsdom": "^29.1.1",
130
149
  "lucide-react": "^1.17.0",
131
150
  "openapi-fetch": "^0.17.0",
132
151
  "openapi-typescript": "^7.13.0",
@@ -138,6 +157,7 @@
138
157
  "tailwindcss": "^4.3.0",
139
158
  "typescript": "^5.9.3",
140
159
  "vite": "^8.0.16",
160
+ "vitest": "^4.1.10",
141
161
  "zustand": "^5.0.14"
142
162
  },
143
163
  "overrides": {
@@ -68,7 +68,9 @@ class _EvalGraph:
68
68
  def index_status(self):
69
69
  return {"vector_counts": {"node": 3, "chunk": 2}}
70
70
 
71
- def search(self, q, limit=20):
71
+ def search(self, q, limit=20, *, allowed_workspaces=None):
72
+ if allowed_workspaces is not None and "personal" not in allowed_workspaces:
73
+ return {"matches": []}
72
74
  return {
73
75
  "matches": [
74
76
  {
@@ -29,12 +29,15 @@ TARGETS = [
29
29
  ("lattice_brain/runtime/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
30
30
  ("latticeai/services/architecture_readiness.py", "regex", r'(ARCHITECTURE_VERSION_TARGET = ")([^"]+)(")'),
31
31
  ("latticeai/services/product_readiness.py", "regex", r'(PRODUCT_VERSION_TARGET = ")([^"]+)(")'),
32
+ ("latticeai/core/legacy_compatibility.py", "regex", r'(LEGACY_COMPATIBILITY_VERSION = ")([^"]+)(")'),
32
33
  ("pyproject.toml", "regex", r'(^version = ")([^"]+)(")'),
33
34
  ("package.json", "json", "version"),
34
35
  ("package-lock.json", "package-lock", None),
35
36
  ("vscode-extension/package.json", "json", "version"),
36
37
  ("vscode-extension/package-lock.json", "package-lock", None),
38
+ ("browser-extension/manifest.json", "json", "version"),
37
39
  ("src-tauri/Cargo.toml", "regex", r'(^version = ")([^"]+)(")'),
40
+ ("src-tauri/Cargo.lock", "regex", r'(name = "lattice-ai-desktop"\nversion = ")([^"]+)(")'),
38
41
  ("src-tauri/tauri.conf.json", "json", "version"),
39
42
  ("static/app/asset-manifest.json", "json", "version"),
40
43
  ]
@@ -0,0 +1,180 @@
1
+ import { chromium } from "playwright";
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ const repoRoot = process.cwd();
7
+ const version = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
8
+ const port = Number(process.env.LTCAI_RELEASE_EVIDENCE_PORT || 4936);
9
+ const baseURL = `http://127.0.0.1:${port}`;
10
+ const root = path.join(repoRoot, "output", "release", `v${version}`);
11
+ const screenshots = path.join(root, "screenshots");
12
+ const videos = path.join(root, "videos");
13
+ const gifs = path.join(root, "gifs");
14
+
15
+ // A release capture must be reproducible and must never retain partial videos
16
+ // from an interrupted prior run.
17
+ fs.rmSync(root, { recursive: true, force: true });
18
+ fs.mkdirSync(screenshots, { recursive: true });
19
+ fs.mkdirSync(videos, { recursive: true });
20
+ fs.mkdirSync(gifs, { recursive: true });
21
+
22
+ const server = spawn(process.execPath, ["tests/visual/mock_server.cjs"], {
23
+ cwd: repoRoot,
24
+ env: { ...process.env, LTCAI_VISUAL_PORT: String(port) },
25
+ stdio: ["ignore", "pipe", "pipe"],
26
+ });
27
+
28
+ async function waitForServer() {
29
+ const deadline = Date.now() + 15000;
30
+ while (Date.now() < deadline) {
31
+ try {
32
+ const res = await fetch(`${baseURL}/health`);
33
+ if (res.ok) return;
34
+ } catch {}
35
+ await new Promise((resolve) => setTimeout(resolve, 150));
36
+ }
37
+ throw new Error(`visual server did not start on ${baseURL}`);
38
+ }
39
+
40
+ async function shot(page, name) {
41
+ await page.waitForTimeout(120);
42
+ await page.screenshot({ path: path.join(screenshots, name), fullPage: true });
43
+ }
44
+
45
+ async function main() {
46
+ await waitForServer();
47
+
48
+ const browser = await chromium.launch({ headless: true });
49
+ const context = await browser.newContext({
50
+ viewport: { width: 1440, height: 920 },
51
+ recordVideo: { dir: videos, size: { width: 1280, height: 818 } },
52
+ });
53
+ const page = await context.newPage();
54
+ page.setDefaultTimeout(15000);
55
+
56
+ await page.addInitScript(() => {
57
+ if (!sessionStorage.getItem("lattice.releaseEvidence.cleared")) {
58
+ localStorage.removeItem("lattice.productFlow.complete");
59
+ localStorage.removeItem("lattice.productFlow.user");
60
+ localStorage.setItem("lattice.language", "ko");
61
+ localStorage.setItem("lattice.mode", "advanced");
62
+ sessionStorage.setItem("lattice.releaseEvidence.cleared", "true");
63
+ }
64
+ });
65
+
66
+ await page.goto(`${baseURL}/app`, { waitUntil: "networkidle" });
67
+ const korean = page.getByRole("button", { name: "한국어" });
68
+ if (await korean.count()) {
69
+ await korean.click();
70
+ }
71
+ await page.getByRole("button", { name: "Brain 지금 깨우기" }).click();
72
+ await page.getByText("이 Brain의 주인을 정합니다.").waitFor();
73
+ await shot(page, "01-login.png");
74
+
75
+ await page.getByPlaceholder(/You|나/).fill("Codex");
76
+ await page.getByPlaceholder("you@local").fill("codex@local");
77
+ await page.getByPlaceholder("로컬 Brain 비밀번호").fill("Lattice123");
78
+ await page.getByRole("button", { name: "내 Brain 시작하기" }).click();
79
+ await page.getByText("추천대로 시작하세요.").waitFor();
80
+ await shot(page, "02-recommended-models.png");
81
+
82
+ await page.getByRole("button", { name: "추천으로 바로 시작" }).click();
83
+ await page.getByText("모델을 준비하고 시작합니다.").waitFor();
84
+ await page.getByRole("button", { name: "준비하고 시작하기" }).click();
85
+ await page.waitForTimeout(180);
86
+ await shot(page, "03-install-load-progress.png");
87
+ await page.locator("main[aria-label='Lattice Brain']").waitFor();
88
+ await shot(page, "04-brain-chat-home.png");
89
+
90
+ await page.goto(`${baseURL}/app#/knowledge-graph`, { waitUntil: "networkidle" });
91
+ await page.locator("[data-testid='brain-cytoscape']").waitFor();
92
+ const graphSearch = page.locator(
93
+ "input[aria-label='Search knowledge graph'], input[placeholder*='Search'], input[placeholder*='검색']"
94
+ ).first();
95
+ if (await graphSearch.count()) {
96
+ await graphSearch.fill("workspace");
97
+ }
98
+ await shot(page, "05-memory-graph.png");
99
+
100
+ await page.goto(`${baseURL}/app#/capture`, { waitUntil: "networkidle" });
101
+ await page.locator("h1.page-title", { hasText: "어떤 자료를 기억할까요?" }).waitFor();
102
+ await shot(page, "06-capture.png");
103
+
104
+ await page.goto(`${baseURL}/app#/models`, { waitUntil: "networkidle" });
105
+ await page.locator("h1.page-title", { hasText: "Lattice가 사용할 AI를 선택하세요." }).waitFor();
106
+ await shot(page, "07-model-library.png");
107
+
108
+ await page.goto(`${baseURL}/app#/settings`, { waitUntil: "networkidle" });
109
+ await page.locator("h1.page-title", { hasText: "Lattice를 내 방식에 맞게 설정하세요." }).waitFor();
110
+ await shot(page, "08-system.png");
111
+
112
+ await page.goto(`${baseURL}/app#/brain`, { waitUntil: "networkidle" });
113
+ await page.locator("main[aria-label='Lattice Brain']").waitFor();
114
+ await page.getByTestId("brain-knowledge-flow").waitFor();
115
+ await page.waitForTimeout(250);
116
+ await shot(page, "09-model-setup-status.png");
117
+
118
+ await page.goto(`${baseURL}/app#/admin/users`, { waitUntil: "networkidle" });
119
+ await page.waitForTimeout(250);
120
+ await shot(page, "10-admin-console.png");
121
+
122
+ await page.goto(`${baseURL}/app#/review`, { waitUntil: "networkidle" });
123
+ await page.getByText(/Review Center|리뷰 센터|검토함/).first().waitFor();
124
+ const rawError = page.getByText(/not valid JSON|Unexpected token/);
125
+ if (await rawError.count()) {
126
+ throw new Error("Review Center release evidence contains a raw API parse error");
127
+ }
128
+ await shot(page, "12-review-center.png");
129
+
130
+ const video = page.video();
131
+ await context.close();
132
+ await browser.close();
133
+
134
+ const recorded = await video.path();
135
+ const webmTarget = path.join(videos, `v${version}-living-brain-walkthrough.webm`);
136
+ const gifTarget = path.join(gifs, `v${version}-living-brain-walkthrough.gif`);
137
+ if (fs.existsSync(webmTarget)) fs.unlinkSync(webmTarget);
138
+ if (fs.existsSync(gifTarget)) fs.unlinkSync(gifTarget);
139
+ fs.renameSync(recorded, webmTarget);
140
+ execFileSync("ffmpeg", [
141
+ "-y",
142
+ "-i", webmTarget,
143
+ "-vf", "fps=8,scale=960:-1:flags=lanczos",
144
+ "-loop", "0",
145
+ gifTarget,
146
+ ], { stdio: "ignore" });
147
+
148
+ const index = `# v${version} Release Evidence
149
+
150
+ Captured from the built React/Vite app served by the release visual API on ${new Date().toISOString()}.
151
+
152
+ ## Screenshots
153
+
154
+ | File | Flow |
155
+ | --- | --- |
156
+ | [01-login.png](screenshots/01-login.png) | Login |
157
+ | [02-recommended-models.png](screenshots/02-recommended-models.png) | Recommended Models |
158
+ | [03-install-load-progress.png](screenshots/03-install-load-progress.png) | Install & Load progress |
159
+ | [04-brain-chat-home.png](screenshots/04-brain-chat-home.png) | Brain Chat home |
160
+ | [05-memory-graph.png](screenshots/05-memory-graph.png) | Memory Graph |
161
+ | [06-capture.png](screenshots/06-capture.png) | Add Sources |
162
+ | [07-model-library.png](screenshots/07-model-library.png) | Model Library |
163
+ | [08-system.png](screenshots/08-system.png) | System |
164
+ | [09-model-setup-status.png](screenshots/09-model-setup-status.png) | Model setup status |
165
+ | [10-admin-console.png](screenshots/10-admin-console.png) | Separate Admin Console |
166
+ | [12-review-center.png](screenshots/12-review-center.png) | Automation Review Center |
167
+
168
+ ## Motion Evidence
169
+
170
+ - [v${version}-living-brain-walkthrough.webm](videos/v${version}-living-brain-walkthrough.webm)
171
+ - [v${version}-living-brain-walkthrough.gif](gifs/v${version}-living-brain-walkthrough.gif)
172
+ `;
173
+ fs.writeFileSync(path.join(root, "SCREENSHOT_INDEX.md"), index);
174
+ }
175
+
176
+ try {
177
+ await main();
178
+ } finally {
179
+ server.kill();
180
+ }