ltcai 3.0.1 → 3.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 (123) hide show
  1. package/README.md +54 -21
  2. package/docs/CHANGELOG.md +90 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +20 -17
  5. package/docs/architecture.md +6 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/agent_registry.py +103 -0
  8. package/latticeai/api/auth.py +4 -1
  9. package/latticeai/api/hooks.py +113 -0
  10. package/latticeai/api/marketplace.py +13 -0
  11. package/latticeai/api/memory.py +109 -0
  12. package/latticeai/api/search.py +4 -0
  13. package/latticeai/core/agent_registry.py +234 -0
  14. package/latticeai/core/config.py +2 -0
  15. package/latticeai/core/embedding_providers.py +123 -0
  16. package/latticeai/core/hooks.py +284 -0
  17. package/latticeai/core/marketplace.py +87 -2
  18. package/latticeai/core/multi_agent.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/server_app.py +63 -6
  21. package/latticeai/services/memory_service.py +324 -0
  22. package/package.json +9 -4
  23. package/scripts/build_v3_assets.mjs +164 -0
  24. package/scripts/capture/README.md +28 -0
  25. package/scripts/capture/capture_enterprise.js +8 -0
  26. package/scripts/capture/capture_graph.js +8 -0
  27. package/scripts/capture/capture_onboarding.js +8 -0
  28. package/scripts/capture/capture_page.js +43 -0
  29. package/scripts/capture/capture_release_media.js +125 -0
  30. package/scripts/capture/capture_skills.js +8 -0
  31. package/scripts/capture/capture_workspace.js +8 -0
  32. package/scripts/generate_diagrams.py +513 -0
  33. package/scripts/lint_v3.mjs +33 -0
  34. package/scripts/release-0.3.1.sh +105 -0
  35. package/scripts/take_screenshots.js +69 -0
  36. package/scripts/validate_release_artifacts.py +167 -0
  37. package/static/account.html +9 -9
  38. package/static/activity.html +4 -4
  39. package/static/admin.html +8 -8
  40. package/static/agents.html +4 -4
  41. package/static/chat.html +9 -9
  42. package/static/css/tokens.5a595671.css +260 -0
  43. package/static/css/tokens.css +1 -1
  44. package/static/graph.html +9 -9
  45. package/static/plugins.html +4 -4
  46. package/static/sw.js +3 -1
  47. package/static/v3/asset-manifest.json +55 -0
  48. package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
  49. package/static/v3/css/lattice.components.011e988b.css +447 -0
  50. package/static/v3/css/lattice.components.css +2 -2
  51. package/static/v3/css/lattice.shell.4920f42d.css +407 -0
  52. package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
  53. package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
  54. package/static/v3/index.html +38 -9
  55. package/static/v3/js/app.a5adc0f3.js +26 -0
  56. package/static/v3/js/core/api.603b978f.js +408 -0
  57. package/static/v3/js/core/api.js +132 -51
  58. package/static/v3/js/core/components.4c83e0a9.js +222 -0
  59. package/static/v3/js/core/components.js +9 -2
  60. package/static/v3/js/core/dom.a2773eb0.js +148 -0
  61. package/static/v3/js/core/router.584570f2.js +37 -0
  62. package/static/v3/js/core/routes.07ad6696.js +89 -0
  63. package/static/v3/js/core/routes.js +17 -1
  64. package/static/v3/js/core/shell.ea0b9ae5.js +363 -0
  65. package/static/v3/js/core/store.34ebd5e6.js +113 -0
  66. package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
  67. package/static/v3/js/views/admin-audit.js +1 -1
  68. package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
  69. package/static/v3/js/views/admin-permissions.js +4 -5
  70. package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
  71. package/static/v3/js/views/admin-policies.js +4 -5
  72. package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
  73. package/static/v3/js/views/admin-private-vpc.js +2 -5
  74. package/static/v3/js/views/admin-security.07c66b72.js +180 -0
  75. package/static/v3/js/views/admin-security.js +4 -5
  76. package/static/v3/js/views/admin-users.03bac88c.js +168 -0
  77. package/static/v3/js/views/admin-users.js +6 -6
  78. package/static/v3/js/views/agents.c373d48c.js +293 -0
  79. package/static/v3/js/views/agents.js +101 -2
  80. package/static/v3/js/views/chat.718144ce.js +449 -0
  81. package/static/v3/js/views/chat.js +2 -3
  82. package/static/v3/js/views/files.4935197e.js +186 -0
  83. package/static/v3/js/views/files.js +27 -21
  84. package/static/v3/js/views/home.cdde3b32.js +119 -0
  85. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  86. package/static/v3/js/views/hooks.js +99 -0
  87. package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
  88. package/static/v3/js/views/hybrid-search.js +1 -1
  89. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
  90. package/static/v3/js/views/knowledge-graph.js +2 -3
  91. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  92. package/static/v3/js/views/marketplace.js +141 -0
  93. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  94. package/static/v3/js/views/mcp.js +114 -0
  95. package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
  96. package/static/v3/js/views/memory.js +146 -0
  97. package/static/v3/js/views/models.a1ffa147.js +256 -0
  98. package/static/v3/js/views/models.js +17 -8
  99. package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
  100. package/static/v3/js/views/my-computer.js +5 -5
  101. package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
  102. package/static/v3/js/views/pipeline.js +3 -7
  103. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  104. package/static/v3/js/views/planning.js +153 -0
  105. package/static/v3/js/views/settings.4f777210.js +250 -0
  106. package/static/v3/js/views/settings.js +6 -14
  107. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  108. package/static/v3/js/views/skills.js +109 -0
  109. package/static/v3/js/views/tools.e4f11276.js +108 -0
  110. package/static/v3/js/views/tools.js +108 -0
  111. package/static/v3/js/views/workflows.26c57290.js +128 -0
  112. package/static/v3/js/views/workflows.js +128 -0
  113. package/static/workflows.html +4 -4
  114. package/static/workspace.html +5 -5
  115. package/docs/images/tmp_frames/frame_00.png +0 -0
  116. package/docs/images/tmp_frames/frame_01.png +0 -0
  117. package/docs/images/tmp_frames/frame_02.png +0 -0
  118. package/docs/images/tmp_frames/frame_03.png +0 -0
  119. package/docs/images/tmp_frames/hero_00.png +0 -0
  120. package/docs/images/tmp_frames/hero_01.png +0 -0
  121. package/docs/images/tmp_frames/hero_02.png +0 -0
  122. package/docs/images/tmp_frames/hero_03.png +0 -0
  123. package/static/v3/js/core/fixtures.js +0 -171
@@ -0,0 +1,324 @@
1
+ """Long-Term Memory platform + Memory Manager (v3.2.0).
2
+
3
+ Parts 7, 8 and 13. Lattice AI already persists memory in several real stores;
4
+ before this service they were unrelated. ``MemoryService`` unifies them behind
5
+ one façade and adds a Memory Manager that reports usage / sources / health /
6
+ size / type and supports recall / inspect / prune / compact / rebuild / clear.
7
+
8
+ Memory tiers and their real backing store (nothing is fabricated — a tier with
9
+ no backing reports ``unavailable``):
10
+
11
+ * **workspace** — personal workspace memories (``WorkspaceOS`` memories)
12
+ * **project** — memories scoped to a non-personal (organization) workspace
13
+ * **agent** — agent memory snapshots captured during runs
14
+ * **conversation** — chat history conversations
15
+ * **graph** — Knowledge Graph nodes (entities + relations)
16
+ * **vector** — local embedding vector index
17
+
18
+ The service never invents counts or health: every number is read from the
19
+ underlying store, and missing stores surface as ``unavailable``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any, Callable, Dict, List, Optional
28
+
29
+ # Personal workspace memory kinds (from WorkspaceOS.MEMORY_KINDS).
30
+ WORKSPACE_KINDS = (
31
+ "short_term",
32
+ "workspace",
33
+ "preferences",
34
+ "decisions",
35
+ "working_style",
36
+ "frequently_used_tools",
37
+ "long_term",
38
+ )
39
+
40
+ TIERS = ("workspace", "project", "agent", "conversation", "graph", "vector")
41
+
42
+
43
+ def _now() -> str:
44
+ return datetime.now().isoformat(timespec="seconds")
45
+
46
+
47
+ def _file_size(path: Path) -> int:
48
+ try:
49
+ return path.stat().st_size if path.exists() else 0
50
+ except Exception:
51
+ return 0
52
+
53
+
54
+ class MemoryService:
55
+ def __init__(
56
+ self,
57
+ *,
58
+ store: Any,
59
+ data_dir: Path,
60
+ knowledge_graph: Any = None,
61
+ enable_graph: bool = True,
62
+ history_file: Optional[Path] = None,
63
+ ):
64
+ self._store = store
65
+ self._kg = knowledge_graph
66
+ self._enable_graph = bool(enable_graph and knowledge_graph is not None)
67
+ self._data_dir = Path(data_dir)
68
+ self._history_file = Path(history_file) if history_file else (self._data_dir / "chat_history.json")
69
+
70
+ # ── helpers over the underlying stores ────────────────────────────────
71
+ def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
72
+ try:
73
+ return list(self._store.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", []))
74
+ except Exception:
75
+ return []
76
+
77
+ def _all_memories(self) -> List[Dict[str, Any]]:
78
+ try:
79
+ return list(self._store.list_memories().get("memories", []))
80
+ except Exception:
81
+ return []
82
+
83
+ def _snapshots(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
84
+ try:
85
+ return list(self._store.list_memory_snapshots(workspace_id=workspace_id, limit=200).get("snapshots", []))
86
+ except Exception:
87
+ return []
88
+
89
+ def _conversations(self) -> List[Dict[str, Any]]:
90
+ if not self._history_file.exists():
91
+ return []
92
+ try:
93
+ with open(self._history_file, "r", encoding="utf-8") as fh:
94
+ data = json.load(fh)
95
+ except Exception:
96
+ return []
97
+ if isinstance(data, dict):
98
+ convs = data.get("conversations")
99
+ if isinstance(convs, list):
100
+ return convs
101
+ return [{"id": k, **(v if isinstance(v, dict) else {"messages": v})} for k, v in data.items()]
102
+ if isinstance(data, list):
103
+ return data
104
+ return []
105
+
106
+ def _kg_stats(self) -> Optional[Dict[str, Any]]:
107
+ if not self._enable_graph:
108
+ return None
109
+ try:
110
+ return self._kg.stats()
111
+ except Exception:
112
+ return None
113
+
114
+ def _kg_index(self) -> Optional[Dict[str, Any]]:
115
+ if not self._enable_graph:
116
+ return None
117
+ try:
118
+ return self._kg.index_status()
119
+ except Exception:
120
+ return None
121
+
122
+ # ── Memory Manager: sources / usage / health ──────────────────────────
123
+ def manager(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
124
+ ws_mem = self._workspace_memories(user_email=user_email, workspace_id="personal")
125
+ project_mem = [m for m in self._all_memories() if (m.get("workspace_id") or "personal") != "personal"]
126
+ snaps = self._snapshots(workspace_id=workspace_id)
127
+ convs = self._conversations()
128
+ kg_stats = self._kg_stats()
129
+ kg_index = self._kg_index()
130
+
131
+ ws_bytes = _file_size(self._data_dir / "workspace_os.json")
132
+ kg_bytes = _file_size(self._data_dir / "knowledge_graph.sqlite")
133
+ conv_bytes = _file_size(self._history_file)
134
+
135
+ node_total = sum((kg_stats or {}).get("nodes", {}).values()) if kg_stats else None
136
+ edge_total = sum((kg_stats or {}).get("edges", {}).values()) if kg_stats else None
137
+ vector_total = None
138
+ if kg_index and isinstance(kg_index.get("vector_counts"), dict):
139
+ vector_total = sum(kg_index["vector_counts"].values())
140
+ elif kg_index:
141
+ vector_total = kg_index.get("indexed") or kg_index.get("ready")
142
+
143
+ sources = [
144
+ {
145
+ "id": "workspace", "type": "workspace", "label": "Workspace Memory",
146
+ "count": len(ws_mem), "size_bytes": ws_bytes if ws_mem else 0,
147
+ "health": "ok", "detail": "Personal workspace knowledge, by kind.",
148
+ },
149
+ {
150
+ "id": "project", "type": "project", "label": "Project Memory",
151
+ "count": len(project_mem), "size_bytes": 0,
152
+ "health": "ok", "detail": "Memory scoped to organization workspaces.",
153
+ },
154
+ {
155
+ "id": "agent", "type": "agent", "label": "Agent Memory",
156
+ "count": len(snaps), "size_bytes": 0,
157
+ "health": "ok", "detail": "Per-run agent memory snapshots.",
158
+ },
159
+ {
160
+ "id": "conversation", "type": "conversation", "label": "Conversation Memory",
161
+ "count": len(convs), "size_bytes": conv_bytes,
162
+ "health": "ok" if self._history_file.exists() else "empty",
163
+ "detail": "Historical interaction memory from chat.",
164
+ },
165
+ {
166
+ "id": "graph", "type": "graph", "label": "Graph Memory",
167
+ "count": node_total, "size_bytes": kg_bytes,
168
+ "health": "ok" if kg_stats else "unavailable",
169
+ "detail": "Knowledge Graph entities and relations." if kg_stats else "Knowledge graph disabled or unavailable.",
170
+ "edges": edge_total,
171
+ },
172
+ {
173
+ "id": "vector", "type": "vector", "label": "Vector Memory",
174
+ "count": vector_total, "size_bytes": 0,
175
+ "health": "ok" if kg_index else "unavailable",
176
+ "detail": "Local embedding vector index." if kg_index else "Vector index unavailable.",
177
+ },
178
+ ]
179
+ total_items = sum((s["count"] or 0) for s in sources)
180
+ total_bytes = ws_bytes + kg_bytes + conv_bytes
181
+ healthy = sum(1 for s in sources if s["health"] == "ok")
182
+ overall = "ok" if healthy >= 4 else "degraded" if healthy >= 1 else "unavailable"
183
+ return {
184
+ "sources": sources,
185
+ "tiers": list(TIERS),
186
+ "usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
187
+ "health": overall,
188
+ "graph_enabled": self._enable_graph,
189
+ "generated_at": _now(),
190
+ }
191
+
192
+ def tiers(self) -> Dict[str, Any]:
193
+ return {"tiers": list(TIERS), "workspace_kinds": list(WORKSPACE_KINDS)}
194
+
195
+ # ── recall (unified retrieval over the memory tiers) ───────────────────
196
+ def recall(
197
+ self,
198
+ query: str,
199
+ *,
200
+ user_email: Optional[str] = None,
201
+ workspace_id: Optional[str] = None,
202
+ limit: int = 20,
203
+ ) -> Dict[str, Any]:
204
+ q = str(query or "").strip()
205
+ results: List[Dict[str, Any]] = []
206
+
207
+ try:
208
+ mem = self._store.search_memories(q, user_email=user_email, limit=limit, workspace_id=workspace_id).get("memories", [])
209
+ except Exception:
210
+ mem = []
211
+ for m in mem:
212
+ results.append({
213
+ "source": "workspace",
214
+ "id": m.get("id"),
215
+ "title": (m.get("kind") or "memory"),
216
+ "snippet": str(m.get("content") or "")[:240],
217
+ "kind": m.get("kind"),
218
+ "score": 0.6,
219
+ "tags": m.get("tags") or [],
220
+ })
221
+
222
+ if self._enable_graph and q:
223
+ try:
224
+ hits = self._kg.search(q, limit).get("results", [])
225
+ except Exception:
226
+ hits = []
227
+ for hsit in hits[:limit]:
228
+ results.append({
229
+ "source": "graph",
230
+ "id": hsit.get("id") or hsit.get("node_id"),
231
+ "title": hsit.get("title") or hsit.get("name") or "node",
232
+ "snippet": str(hsit.get("summary") or hsit.get("content") or "")[:240],
233
+ "kind": hsit.get("type") or "node",
234
+ "score": float(hsit.get("score") or 0.5),
235
+ })
236
+
237
+ results.sort(key=lambda r: r.get("score", 0), reverse=True)
238
+ return {"query": q, "results": results[: max(1, min(limit, 100))], "count": len(results), "source": "live"}
239
+
240
+ # ── inspect a single tier ─────────────────────────────────────────────
241
+ def inspect(self, source: str, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
242
+ if source == "workspace":
243
+ items = self._workspace_memories(user_email=user_email, workspace_id="personal")[:limit]
244
+ return {"source": source, "items": items, "count": len(items)}
245
+ if source == "project":
246
+ items = [m for m in self._all_memories() if (m.get("workspace_id") or "personal") != "personal"][:limit]
247
+ return {"source": source, "items": items, "count": len(items)}
248
+ if source == "agent":
249
+ items = self._snapshots(workspace_id=workspace_id)[:limit]
250
+ return {"source": source, "items": items, "count": len(items)}
251
+ if source == "conversation":
252
+ convs = self._conversations()
253
+ items = [{"id": c.get("id"), "title": c.get("title") or c.get("id"), "messages": len(c.get("messages") or [])} for c in convs[:limit]]
254
+ return {"source": source, "items": items, "count": len(convs)}
255
+ if source == "graph":
256
+ return {"source": source, "stats": self._kg_stats() or {}, "available": bool(self._kg_stats())}
257
+ if source == "vector":
258
+ return {"source": source, "index": self._kg_index() or {}, "available": bool(self._kg_index())}
259
+ raise KeyError(source)
260
+
261
+ # ── mutating operations ───────────────────────────────────────────────
262
+ def prune(self, *, ids: Optional[List[str]] = None, kind: Optional[str] = None, user_email: Optional[str] = None) -> Dict[str, Any]:
263
+ removed: List[str] = []
264
+ target_ids = list(ids or [])
265
+ if kind:
266
+ for m in self._workspace_memories(user_email=user_email, workspace_id=None):
267
+ if m.get("kind") == kind and m.get("id"):
268
+ target_ids.append(m["id"])
269
+ for mid in target_ids:
270
+ try:
271
+ self._store.delete_memory(mid)
272
+ removed.append(mid)
273
+ except Exception:
274
+ continue
275
+ return {"removed": removed, "count": len(removed)}
276
+
277
+ def compact(self, *, user_email: Optional[str] = None) -> Dict[str, Any]:
278
+ """Dedupe workspace memories with identical (kind, content)."""
279
+ seen: set = set()
280
+ removed: List[str] = []
281
+ # Oldest first so the first occurrence (oldest) is kept.
282
+ memories = list(reversed(self._workspace_memories(user_email=user_email, workspace_id=None)))
283
+ for m in memories:
284
+ key = (m.get("kind"), str(m.get("content") or "").strip())
285
+ if key in seen:
286
+ if m.get("id"):
287
+ try:
288
+ self._store.delete_memory(m["id"])
289
+ removed.append(m["id"])
290
+ except Exception:
291
+ continue
292
+ else:
293
+ seen.add(key)
294
+ return {"compacted": len(removed), "removed": removed, "remaining": len(seen)}
295
+
296
+ def rebuild(self, target: str = "vector") -> Dict[str, Any]:
297
+ if target in {"vector", "index", "vector_index"}:
298
+ if not self._enable_graph:
299
+ return {"status": "unavailable", "detail": "Knowledge graph / vector index disabled."}
300
+ try:
301
+ result = self._kg.rebuild_vector_index()
302
+ return {"status": "ok", "target": "vector_index", "result": result}
303
+ except Exception as exc:
304
+ return {"status": "error", "detail": str(exc)}
305
+ return {"status": "error", "detail": f"Unknown rebuild target: {target}"}
306
+
307
+ def clear(self, *, scope: str, confirm: bool = False, user_email: Optional[str] = None) -> Dict[str, Any]:
308
+ if not confirm:
309
+ raise ValueError("clear requires confirm=true")
310
+ if scope in WORKSPACE_KINDS:
311
+ result = self.prune(kind=scope, user_email=user_email)
312
+ return {"cleared": scope, **result}
313
+ if scope == "workspace":
314
+ ids = [m["id"] for m in self._workspace_memories(user_email=user_email, workspace_id=None) if m.get("id")]
315
+ result = self.prune(ids=ids, user_email=user_email)
316
+ return {"cleared": "workspace", **result}
317
+ if scope == "graph":
318
+ if not self._enable_graph:
319
+ return {"status": "unavailable", "detail": "Knowledge graph disabled."}
320
+ try:
321
+ return {"cleared": "graph", "result": self._kg.clear_all()}
322
+ except Exception as exc:
323
+ return {"status": "error", "detail": str(exc)}
324
+ raise ValueError(f"unsupported clear scope: {scope}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Lattice AI v3 local-first AI workspace platform with knowledge graph, vector index, hybrid search, agents, and workspace modes.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -17,9 +17,10 @@
17
17
  "scripts": {
18
18
  "start": "LTCAI",
19
19
  "dev": "python3 ltcai_cli.py --reload",
20
- "build": "npm run build:python",
20
+ "build": "npm run build:assets && npm run build:python",
21
+ "build:assets": "node scripts/build_v3_assets.mjs",
21
22
  "build:python": "python3 -m build",
22
- "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/api/marketplace.py latticeai/api/search.py latticeai/services/search_service.py latticeai/core/local_embeddings.py latticeai/core/embedding_providers.py latticeai/services/agent_runtime.py latticeai/core/config.py latticeai/api/admin.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/marketplace.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
23
+ "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/auth.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/api/marketplace.py latticeai/api/search.py latticeai/services/search_service.py latticeai/core/local_embeddings.py latticeai/core/embedding_providers.py latticeai/services/agent_runtime.py latticeai/core/config.py latticeai/api/admin.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/marketplace.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py latticeai/api/mcp.py latticeai/core/hooks.py latticeai/api/hooks.py latticeai/core/agent_registry.py latticeai/api/agent_registry.py latticeai/services/memory_service.py latticeai/api/memory.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
23
24
  "lint": "node --check static/scripts/account.js && node --check static/scripts/admin.js && node --check static/scripts/chat.js && node --check static/scripts/graph.js && node --check static/scripts/platform.js && node --check static/scripts/ux.js && node --check static/scripts/workspace.js && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
24
25
  "lint:v3": "node scripts/lint_v3.mjs",
25
26
  "typecheck": "cd vscode-extension && npm run build",
@@ -32,7 +33,7 @@
32
33
  "capture:skills": "node scripts/capture/capture_skills.js",
33
34
  "capture:enterprise": "node scripts/capture/capture_enterprise.js",
34
35
  "capture:onboarding": "node scripts/capture/capture_onboarding.js",
35
- "release:artifacts": "npm run build:python && npm pack && cd vscode-extension && npm run package:vsix",
36
+ "release:artifacts": "npm run build:assets && npm run build:python && npm pack && cd vscode-extension && npm run package:vsix",
36
37
  "release:validate": "python3 scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
37
38
  "publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
38
39
  "publish:pypi": "npm run build:python && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
@@ -93,7 +94,11 @@
93
94
  "static/v3/",
94
95
  "static/icons/",
95
96
  "plugins/",
97
+ "scripts/",
96
98
  "docs/",
99
+ "!docs/images/tmp_frames/",
100
+ "!**/__pycache__/",
101
+ "!**/*.pyc",
97
102
  "requirements.txt",
98
103
  "README.md"
99
104
  ],
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Build the v3 browser asset manifest.
4
+ *
5
+ * The source files stay importable in development. This script writes hashed
6
+ * siblings next to each runtime asset, rewrites ES-module imports to those
7
+ * hashed siblings, and emits static/v3/asset-manifest.json for /app.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ rmSync,
16
+ statSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { basename, dirname, extname, join, relative } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
23
+ const staticRoot = join(repoRoot, "static");
24
+ const manifestPath = join(staticRoot, "v3", "asset-manifest.json");
25
+
26
+ const cssSources = [
27
+ "static/css/tokens.css",
28
+ "static/v3/css/lattice.tokens.css",
29
+ "static/v3/css/lattice.base.css",
30
+ "static/v3/css/lattice.components.css",
31
+ "static/v3/css/lattice.shell.css",
32
+ "static/v3/css/lattice.views.css",
33
+ ];
34
+
35
+ const moduleRoot = join(staticRoot, "v3", "js");
36
+ const entry = "static/v3/js/app.js";
37
+
38
+ function posix(p) {
39
+ return p.replaceAll("\\", "/");
40
+ }
41
+
42
+ function sha(text) {
43
+ return createHash("sha256").update(text).digest("hex").slice(0, 8);
44
+ }
45
+
46
+ function repoPath(abs) {
47
+ return posix(relative(repoRoot, abs));
48
+ }
49
+
50
+ function publicUrl(repoRel) {
51
+ return "/" + posix(repoRel);
52
+ }
53
+
54
+ function walk(dir) {
55
+ const out = [];
56
+ for (const name of readdirSync(dir)) {
57
+ const p = join(dir, name);
58
+ const st = statSync(p);
59
+ if (st.isDirectory()) out.push(...walk(p));
60
+ else if (name.endsWith(".js")) out.push(p);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function removeGenerated(dir, ext) {
66
+ if (!existsSync(dir)) return;
67
+ for (const name of readdirSync(dir)) {
68
+ const p = join(dir, name);
69
+ const st = statSync(p);
70
+ if (st.isDirectory()) removeGenerated(p, ext);
71
+ else if (new RegExp(`\\.[0-9a-f]{8}\\${ext}$`).test(name)) rmSync(p);
72
+ }
73
+ }
74
+
75
+ removeGenerated(join(staticRoot, "css"), ".css");
76
+ removeGenerated(join(staticRoot, "v3", "css"), ".css");
77
+ removeGenerated(moduleRoot, ".js");
78
+
79
+ const modules = new Map();
80
+ for (const abs of walk(moduleRoot)) {
81
+ modules.set(repoPath(abs), {
82
+ abs,
83
+ rel: repoPath(abs),
84
+ source: readFileSync(abs, "utf8"),
85
+ deps: [],
86
+ });
87
+ }
88
+
89
+ const importFromRe = /\b(?:import|export)\s+(?:[^"'()]*?\s+from\s*)?["']([^"']+\.js)["']/g;
90
+ for (const mod of modules.values()) {
91
+ const deps = [];
92
+ let match;
93
+ while ((match = importFromRe.exec(mod.source))) {
94
+ const spec = match[1];
95
+ if (!spec.startsWith(".")) continue;
96
+ const depRel = repoPath(join(dirname(mod.abs), spec));
97
+ if (modules.has(depRel)) deps.push(depRel);
98
+ }
99
+ mod.deps = deps;
100
+ }
101
+
102
+ const hashMemo = new Map();
103
+ function moduleHash(rel, stack = []) {
104
+ if (hashMemo.has(rel)) return hashMemo.get(rel);
105
+ if (stack.includes(rel)) return sha(modules.get(rel).source);
106
+ const mod = modules.get(rel);
107
+ const depHashes = mod.deps
108
+ .sort()
109
+ .map((dep) => `${dep}:${moduleHash(dep, [...stack, rel])}`)
110
+ .join("\n");
111
+ const h = sha(`${mod.source}\n/* dependency-hashes */\n${depHashes}`);
112
+ hashMemo.set(rel, h);
113
+ return h;
114
+ }
115
+
116
+ for (const rel of modules.keys()) moduleHash(rel);
117
+
118
+ function hashedRel(rel, hash) {
119
+ const ext = extname(rel);
120
+ return posix(join(dirname(rel), `${basename(rel, ext)}.${hash}${ext}`));
121
+ }
122
+
123
+ const assets = {};
124
+
125
+ for (const sourceRel of cssSources) {
126
+ const abs = join(repoRoot, sourceRel);
127
+ const source = readFileSync(abs, "utf8");
128
+ const outRel = hashedRel(sourceRel, sha(source));
129
+ writeFileSync(join(repoRoot, outRel), source, "utf8");
130
+ assets[sourceRel] = publicUrl(outRel);
131
+ }
132
+
133
+ function rewriteModule(mod) {
134
+ return mod.source.replace(importFromRe, (full, spec) => {
135
+ if (!spec.startsWith(".")) return full;
136
+ const depRel = repoPath(join(dirname(mod.abs), spec));
137
+ const depHash = hashMemo.get(depRel);
138
+ if (!depHash) return full;
139
+ const depOutAbs = join(repoRoot, hashedRel(depRel, depHash));
140
+ const nextSpec = posix(relative(dirname(join(repoRoot, hashedRel(mod.rel, hashMemo.get(mod.rel)))), depOutAbs));
141
+ const normalized = nextSpec.startsWith(".") ? nextSpec : `./${nextSpec}`;
142
+ return full.replace(spec, normalized);
143
+ });
144
+ }
145
+
146
+ for (const mod of modules.values()) {
147
+ const outRel = hashedRel(mod.rel, hashMemo.get(mod.rel));
148
+ mkdirSync(dirname(join(repoRoot, outRel)), { recursive: true });
149
+ writeFileSync(join(repoRoot, outRel), rewriteModule(mod), "utf8");
150
+ assets[mod.rel] = publicUrl(outRel);
151
+ }
152
+
153
+ const manifest = {
154
+ version: "3.2.0",
155
+ generated_at: "deterministic",
156
+ entrypoints: {
157
+ app: assets[entry],
158
+ styles: cssSources.map((rel) => assets[rel]),
159
+ },
160
+ assets,
161
+ };
162
+
163
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
164
+ console.log(`wrote ${repoPath(manifestPath)} with ${Object.keys(assets).length} assets`);
@@ -0,0 +1,28 @@
1
+ # Screenshot Capture
2
+
3
+ Reproducible Playwright capture scripts for release screenshots.
4
+
5
+ Prerequisites:
6
+
7
+ ```bash
8
+ npm ci
9
+ npx playwright install chromium
10
+ LTCAI
11
+ ```
12
+
13
+ Optional environment:
14
+
15
+ - `LTCAI_CAPTURE_BASE_URL` defaults to `http://localhost:4825`
16
+ - `SESSION_TOKEN` or `LTCAI_SESSION_TOKEN` injects an authenticated session cookie
17
+ - `LTCAI_CAPTURE_OUT` defaults to `docs/images`
18
+ - `LTCAI_CAPTURE_HEADED=1` runs with a visible browser
19
+
20
+ Commands:
21
+
22
+ ```bash
23
+ npm run capture:workspace
24
+ npm run capture:graph
25
+ npm run capture:skills
26
+ npm run capture:enterprise
27
+ npm run capture:onboarding
28
+ ```
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ const { capturePage } = require("./capture_page");
3
+
4
+ capturePage({ path: "/admin#enterprise", waitFor: "#enterprise-capability-status", filename: "enterprise.png" })
5
+ .catch((error) => {
6
+ console.error(error);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ const { capturePage } = require("./capture_page");
3
+
4
+ capturePage({ path: "/graph", waitFor: "#graph", filename: "graph.png", settleMs: 1400 })
5
+ .catch((error) => {
6
+ console.error(error);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ const { capturePage } = require("./capture_page");
3
+
4
+ capturePage({ path: "/onboarding", waitFor: "#onboarding-steps", filename: "onboarding.png" })
5
+ .catch((error) => {
6
+ console.error(error);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,43 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ async function loadPlaywright() {
5
+ try {
6
+ return require("@playwright/test");
7
+ } catch (_) {
8
+ return require("playwright");
9
+ }
10
+ }
11
+
12
+ async function capturePage(options) {
13
+ const { chromium } = await loadPlaywright();
14
+ const baseURL = process.env.LTCAI_CAPTURE_BASE_URL || "http://localhost:4825";
15
+ const outDir = path.resolve(process.env.LTCAI_CAPTURE_OUT || path.join(__dirname, "..", "..", "docs", "images"));
16
+ const sessionToken = process.env.SESSION_TOKEN || process.env.LTCAI_SESSION_TOKEN || "";
17
+ fs.mkdirSync(outDir, { recursive: true });
18
+
19
+ const browser = await chromium.launch({ headless: process.env.LTCAI_CAPTURE_HEADED !== "1" });
20
+ const context = await browser.newContext({
21
+ viewport: { width: Number(process.env.LTCAI_CAPTURE_WIDTH || 1440), height: Number(process.env.LTCAI_CAPTURE_HEIGHT || 920) },
22
+ deviceScaleFactor: Number(process.env.LTCAI_CAPTURE_SCALE || 2),
23
+ });
24
+ if (sessionToken) {
25
+ const host = new URL(baseURL).hostname;
26
+ await context.addCookies([{ name: "session_token", value: sessionToken, domain: host, path: "/", httpOnly: true, secure: false }]);
27
+ }
28
+
29
+ const page = await context.newPage();
30
+ const target = new URL(options.path, baseURL).toString();
31
+ await page.goto(target, { waitUntil: "networkidle", timeout: Number(process.env.LTCAI_CAPTURE_TIMEOUT || 30_000) });
32
+ if (options.hash) await page.evaluate((hash) => { location.hash = hash; }, options.hash);
33
+ if (options.waitFor) await page.waitForSelector(options.waitFor, { timeout: 15_000 });
34
+ await page.waitForTimeout(Number(process.env.LTCAI_CAPTURE_SETTLE_MS || options.settleMs || 900));
35
+
36
+ const outPath = path.join(outDir, options.filename);
37
+ await page.screenshot({ path: outPath, fullPage: Boolean(options.fullPage) });
38
+ await browser.close();
39
+ console.log(outPath);
40
+ return outPath;
41
+ }
42
+
43
+ module.exports = { capturePage };