ltcai 4.0.0 → 4.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 (195) hide show
  1. package/README.md +42 -33
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +106 -0
  4. package/docs/REALTIME_COLLABORATION.md +3 -3
  5. package/docs/V3_FRONTEND.md +9 -8
  6. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  7. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  8. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  9. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +95 -45
  10. package/docs/kg-schema.md +6 -2
  11. package/docs/spec-vs-impl.md +10 -10
  12. package/frontend/index.html +24 -0
  13. package/frontend/openapi.json +14190 -0
  14. package/frontend/src/App.tsx +184 -0
  15. package/frontend/src/api/client.ts +317 -0
  16. package/frontend/src/api/openapi.ts +16637 -0
  17. package/frontend/src/components/primitives.tsx +204 -0
  18. package/frontend/src/components/ui/badge.tsx +27 -0
  19. package/frontend/src/components/ui/button.tsx +37 -0
  20. package/frontend/src/components/ui/card.tsx +22 -0
  21. package/frontend/src/components/ui/input.tsx +16 -0
  22. package/frontend/src/components/ui/textarea.tsx +16 -0
  23. package/frontend/src/lib/utils.ts +33 -0
  24. package/frontend/src/main.tsx +23 -0
  25. package/frontend/src/pages/Act.tsx +245 -0
  26. package/frontend/src/pages/Ask.tsx +200 -0
  27. package/frontend/src/pages/Brain.tsx +267 -0
  28. package/frontend/src/pages/Capture.tsx +158 -0
  29. package/frontend/src/pages/Library.tsx +187 -0
  30. package/frontend/src/pages/System.tsx +344 -0
  31. package/frontend/src/routes.ts +85 -0
  32. package/frontend/src/store/appStore.ts +54 -0
  33. package/frontend/src/styles.css +107 -0
  34. package/kg_schema.py +2 -603
  35. package/knowledge_graph.py +37 -4958
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +15 -16
  38. package/latticeai/api/agents.py +13 -6
  39. package/latticeai/api/auth.py +19 -11
  40. package/latticeai/api/invitations.py +100 -0
  41. package/latticeai/api/knowledge_graph.py +4 -11
  42. package/latticeai/api/plugins.py +3 -6
  43. package/latticeai/api/realtime.py +4 -7
  44. package/latticeai/api/setup.py +5 -4
  45. package/latticeai/api/static_routes.py +13 -16
  46. package/latticeai/api/ui_redirects.py +26 -0
  47. package/latticeai/api/workflow_designer.py +39 -6
  48. package/latticeai/api/workspace.py +24 -10
  49. package/latticeai/app_factory.py +88 -17
  50. package/latticeai/brain/_kg_common.py +1123 -0
  51. package/latticeai/brain/discovery.py +1455 -0
  52. package/latticeai/brain/documents.py +218 -0
  53. package/latticeai/brain/ingest.py +644 -0
  54. package/latticeai/brain/projection.py +561 -0
  55. package/latticeai/brain/provenance.py +401 -0
  56. package/latticeai/brain/retrieval.py +1316 -0
  57. package/latticeai/brain/schema.py +640 -0
  58. package/latticeai/brain/store.py +216 -0
  59. package/latticeai/brain/write_master.py +225 -0
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/multi_agent.py +1 -1
  63. package/latticeai/core/policy.py +54 -0
  64. package/latticeai/core/realtime.py +65 -44
  65. package/latticeai/core/sessions.py +31 -5
  66. package/latticeai/core/users.py +147 -0
  67. package/latticeai/core/workspace_os.py +420 -20
  68. package/latticeai/services/agent_runtime.py +242 -4
  69. package/latticeai/services/run_executor.py +328 -0
  70. package/latticeai/services/workspace_service.py +27 -19
  71. package/package.json +54 -27
  72. package/scripts/build_frontend_assets.mjs +38 -0
  73. package/scripts/bump_version.py +1 -1
  74. package/scripts/export_openapi.py +31 -0
  75. package/scripts/lint_frontend.mjs +86 -0
  76. package/scripts/run_python.mjs +47 -0
  77. package/src-tauri/Cargo.lock +4833 -0
  78. package/src-tauri/Cargo.toml +19 -0
  79. package/src-tauri/build.rs +3 -0
  80. package/src-tauri/capabilities/default.json +7 -0
  81. package/src-tauri/src/main.rs +78 -0
  82. package/src-tauri/tauri.conf.json +36 -0
  83. package/static/app/asset-manifest.json +32 -0
  84. package/static/app/assets/core-CwxXejkd.js +2 -0
  85. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  86. package/static/app/assets/index-CJRAzNnf.js +333 -0
  87. package/static/app/assets/index-CJRAzNnf.js.map +1 -0
  88. package/static/app/assets/index-CSwBBgf4.css +2 -0
  89. package/static/app/index.html +25 -0
  90. package/static/manifest.json +2 -2
  91. package/static/sw.js +4 -4
  92. package/scripts/build_v3_assets.mjs +0 -170
  93. package/scripts/lint_v3.mjs +0 -97
  94. package/static/account.html +0 -113
  95. package/static/activity.html +0 -73
  96. package/static/admin.html +0 -486
  97. package/static/agents.html +0 -139
  98. package/static/chat.html +0 -841
  99. package/static/css/reference/account.css +0 -439
  100. package/static/css/reference/admin.css +0 -610
  101. package/static/css/reference/base.css +0 -1661
  102. package/static/css/reference/chat.css +0 -4623
  103. package/static/css/reference/graph.css +0 -1016
  104. package/static/css/responsive.css +0 -861
  105. package/static/graph.html +0 -122
  106. package/static/platform.css +0 -104
  107. package/static/plugins.html +0 -136
  108. package/static/scripts/account.js +0 -238
  109. package/static/scripts/admin.js +0 -1614
  110. package/static/scripts/chat.js +0 -5081
  111. package/static/scripts/graph.js +0 -1804
  112. package/static/scripts/platform.js +0 -64
  113. package/static/scripts/ux.js +0 -167
  114. package/static/scripts/workspace.js +0 -948
  115. package/static/v3/asset-manifest.json +0 -56
  116. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  117. package/static/v3/css/lattice.base.css +0 -128
  118. package/static/v3/css/lattice.components.cde18231.css +0 -472
  119. package/static/v3/css/lattice.components.css +0 -472
  120. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  121. package/static/v3/css/lattice.shell.css +0 -452
  122. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  123. package/static/v3/css/lattice.tokens.css +0 -135
  124. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  125. package/static/v3/css/lattice.views.css +0 -360
  126. package/static/v3/index.html +0 -68
  127. package/static/v3/js/app.356e6452.js +0 -26
  128. package/static/v3/js/app.js +0 -26
  129. package/static/v3/js/core/api.7a308b89.js +0 -568
  130. package/static/v3/js/core/api.js +0 -568
  131. package/static/v3/js/core/components.f25b3b93.js +0 -230
  132. package/static/v3/js/core/components.js +0 -230
  133. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  134. package/static/v3/js/core/dom.js +0 -148
  135. package/static/v3/js/core/router.584570f2.js +0 -37
  136. package/static/v3/js/core/router.js +0 -37
  137. package/static/v3/js/core/routes.7222343d.js +0 -93
  138. package/static/v3/js/core/routes.js +0 -93
  139. package/static/v3/js/core/shell.a1657f20.js +0 -391
  140. package/static/v3/js/core/shell.js +0 -391
  141. package/static/v3/js/core/store.204a08b2.js +0 -113
  142. package/static/v3/js/core/store.js +0 -113
  143. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  144. package/static/v3/js/views/admin-audit.js +0 -185
  145. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  146. package/static/v3/js/views/admin-permissions.js +0 -177
  147. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  148. package/static/v3/js/views/admin-policies.js +0 -102
  149. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  150. package/static/v3/js/views/admin-private-vpc.js +0 -135
  151. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  152. package/static/v3/js/views/admin-security.js +0 -180
  153. package/static/v3/js/views/admin-users.03bac88c.js +0 -168
  154. package/static/v3/js/views/admin-users.js +0 -168
  155. package/static/v3/js/views/agents.014d0b74.js +0 -541
  156. package/static/v3/js/views/agents.js +0 -541
  157. package/static/v3/js/views/chat.e6dd7dd0.js +0 -601
  158. package/static/v3/js/views/chat.js +0 -601
  159. package/static/v3/js/views/files.adad14c1.js +0 -365
  160. package/static/v3/js/views/files.js +0 -365
  161. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  162. package/static/v3/js/views/graph-canvas.js +0 -509
  163. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  164. package/static/v3/js/views/home.js +0 -200
  165. package/static/v3/js/views/hooks.37895880.js +0 -220
  166. package/static/v3/js/views/hooks.js +0 -220
  167. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  168. package/static/v3/js/views/hybrid-search.js +0 -194
  169. package/static/v3/js/views/knowledge-graph.5e40cbeb.js +0 -509
  170. package/static/v3/js/views/knowledge-graph.js +0 -509
  171. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  172. package/static/v3/js/views/marketplace.js +0 -141
  173. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  174. package/static/v3/js/views/mcp.js +0 -114
  175. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  176. package/static/v3/js/views/memory.js +0 -147
  177. package/static/v3/js/views/models.a1ffa147.js +0 -256
  178. package/static/v3/js/views/models.js +0 -256
  179. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  180. package/static/v3/js/views/my-computer.js +0 -463
  181. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  182. package/static/v3/js/views/pipeline.js +0 -157
  183. package/static/v3/js/views/planning.9ac3e313.js +0 -153
  184. package/static/v3/js/views/planning.js +0 -153
  185. package/static/v3/js/views/settings.8631fa5e.js +0 -318
  186. package/static/v3/js/views/settings.js +0 -318
  187. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  188. package/static/v3/js/views/skills.js +0 -109
  189. package/static/v3/js/views/tools.e4f11276.js +0 -108
  190. package/static/v3/js/views/tools.js +0 -108
  191. package/static/v3/js/views/workflows.26c57290.js +0 -128
  192. package/static/v3/js/views/workflows.js +0 -128
  193. package/static/workflows.html +0 -146
  194. package/static/workspace.css +0 -1121
  195. package/static/workspace.html +0 -357
@@ -26,7 +26,7 @@ Guardrail summary (v1.2.0):
26
26
 
27
27
  from __future__ import annotations
28
28
 
29
- from typing import Any, Dict, Optional
29
+ from typing import Any, Callable, Dict, Optional
30
30
 
31
31
  from latticeai.core.workspace_os import WorkspaceOSStore
32
32
 
@@ -38,13 +38,20 @@ class WorkspaceService:
38
38
  # partitioned per workspace. Surfaced so the UI / docs can be explicit.
39
39
  SHARED_GLOBAL_AREAS = ("graph", "skills")
40
40
 
41
- def __init__(self, store: WorkspaceOSStore):
41
+ def __init__(self, store: WorkspaceOSStore, *, resolve_user_id: Optional[Callable[[Optional[str]], Optional[str]]] = None):
42
42
  self.store = store
43
+ self._resolve_user_id = resolve_user_id or (lambda user_id: user_id)
44
+
45
+ def _identity(self, user_id: Optional[str]) -> Optional[str]:
46
+ if isinstance(user_id, str) and user_id.startswith("user:"):
47
+ return user_id
48
+ return self._resolve_user_id(user_id)
43
49
 
44
50
  # ── scope resolution + gating ────────────────────────────────────────
45
51
 
46
52
  def _ensure_permission(self, workspace_id: str, user_id: Optional[str], permission: str) -> None:
47
- if not self.store.has_permission(workspace_id, user_id, permission):
53
+ resolved_user = self._identity(user_id)
54
+ if not self.store.has_permission(workspace_id, resolved_user, permission):
48
55
  raise PermissionError(
49
56
  f"'{user_id or 'anonymous'}' lacks '{permission}' on workspace '{workspace_id}'"
50
57
  )
@@ -67,10 +74,10 @@ class WorkspaceService:
67
74
  return workspace_id
68
75
 
69
76
  def can_read(self, workspace_id: str, user_id: Optional[str]) -> bool:
70
- return self.store.has_permission(workspace_id, user_id, "read")
77
+ return self.store.has_permission(workspace_id, self._identity(user_id), "read")
71
78
 
72
79
  def can_write(self, workspace_id: str, user_id: Optional[str]) -> bool:
73
- return self.store.has_permission(workspace_id, user_id, "write")
80
+ return self.store.has_permission(workspace_id, self._identity(user_id), "write")
74
81
 
75
82
  # ── record-level authorization (by-id access must not bypass gating) ──
76
83
 
@@ -93,12 +100,13 @@ class WorkspaceService:
93
100
  """
94
101
  owner = (record or {}).get("user_email")
95
102
  workspace_id = (record or {}).get("workspace_id")
96
- if owner and owner == user_id:
103
+ resolved_user = self._identity(user_id)
104
+ if owner and owner in {user_id, resolved_user}:
97
105
  return
98
106
  if workspace_id:
99
- self._ensure_permission(workspace_id, user_id, "write")
107
+ self._ensure_permission(workspace_id, resolved_user, "write")
100
108
  return
101
- if owner and owner != user_id:
109
+ if owner and owner not in {user_id, resolved_user}:
102
110
  raise PermissionError(
103
111
  f"'{user_id or 'anonymous'}' is not the owner of memory '{record.get('id')}'"
104
112
  )
@@ -107,42 +115,42 @@ class WorkspaceService:
107
115
 
108
116
  def summary(self, user_id: Optional[str]) -> Dict[str, Any]:
109
117
  data = self.store.summary()
110
- data["workspace_registry"] = self.store.list_workspaces(user_id=user_id)
118
+ data["workspace_registry"] = self.store.list_workspaces(user_id=self._identity(user_id))
111
119
  data["shared_global_areas"] = list(self.SHARED_GLOBAL_AREAS)
112
120
  return data
113
121
 
114
122
  def list_workspaces(self, user_id: Optional[str]) -> Dict[str, Any]:
115
- return self.store.list_workspaces(user_id=user_id)
123
+ return self.store.list_workspaces(user_id=self._identity(user_id))
116
124
 
117
125
  def get_workspace(self, workspace_id: str, user_id: Optional[str]) -> Dict[str, Any]:
118
126
  # Reading workspace metadata requires read access to that workspace.
119
127
  self._ensure_permission(workspace_id, user_id, "read")
120
- return self.store.get_workspace(workspace_id, user_id=user_id)
128
+ return self.store.get_workspace(workspace_id, user_id=self._identity(user_id))
121
129
 
122
130
  def workspace_summary(self, workspace_id: str, user_id: Optional[str]) -> Dict[str, Any]:
123
131
  self._ensure_permission(workspace_id, user_id, "read")
124
- return self.store.workspace_summary(workspace_id, user_id=user_id)
132
+ return self.store.workspace_summary(workspace_id, user_id=self._identity(user_id))
125
133
 
126
134
  # ── organization workspace management (delegates with actor) ─────────
127
135
 
128
136
  def create_organization_workspace(self, *, name: str, owner_user_id: Optional[str], settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
129
- return self.store.create_organization_workspace(name=name, owner_user_id=owner_user_id, settings=settings)
137
+ return self.store.create_organization_workspace(name=name, owner_user_id=self._identity(owner_user_id), settings=settings)
130
138
 
131
139
  def update_workspace(self, workspace_id: str, *, name=None, settings=None, actor=None) -> Dict[str, Any]:
132
- return self.store.update_workspace(workspace_id, name=name, settings=settings, actor=actor)
140
+ return self.store.update_workspace(workspace_id, name=name, settings=settings, actor=self._identity(actor))
133
141
 
134
142
  def archive_workspace(self, workspace_id: str, *, actor=None) -> Dict[str, Any]:
135
- return self.store.archive_workspace(workspace_id, actor=actor)
143
+ return self.store.archive_workspace(workspace_id, actor=self._identity(actor))
136
144
 
137
145
  def add_member(self, workspace_id: str, *, user_id: str, role: str = "member", actor=None) -> Dict[str, Any]:
138
- return self.store.add_member(workspace_id, user_id=user_id, role=role, actor=actor)
146
+ return self.store.add_member(workspace_id, user_id=self._identity(user_id) or user_id, role=role, actor=self._identity(actor))
139
147
 
140
148
  def update_member_role(self, workspace_id: str, *, user_id: str, role: str, actor=None) -> Dict[str, Any]:
141
- return self.store.update_member_role(workspace_id, user_id=user_id, role=role, actor=actor)
149
+ return self.store.update_member_role(workspace_id, user_id=self._identity(user_id) or user_id, role=role, actor=self._identity(actor))
142
150
 
143
151
  def remove_member(self, workspace_id: str, *, user_id: str, actor=None) -> Dict[str, Any]:
144
- return self.store.remove_member(workspace_id, user_id=user_id, actor=actor)
152
+ return self.store.remove_member(workspace_id, user_id=self._identity(user_id) or user_id, actor=self._identity(actor))
145
153
 
146
154
  def set_active_workspace(self, workspace_id: str, user_id: Optional[str]) -> Dict[str, Any]:
147
155
  # Membership is enforced inside the store for organization workspaces.
148
- return self.store.set_active_workspace(workspace_id, user_id=user_id)
156
+ return self.store.set_active_workspace(workspace_id, user_id=self._identity(user_id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -18,29 +18,36 @@
18
18
  "start": "LTCAI",
19
19
  "dev": "python3 ltcai_cli.py --reload",
20
20
  "build": "npm run build:assets && npm run build:python",
21
- "build:assets": "node scripts/build_v3_assets.mjs",
22
- "build:python": "python3 -m build",
23
- "check:python": "python3 scripts/check_python.py",
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",
25
- "lint:v3": "node scripts/lint_v3.mjs",
26
- "typecheck": "cd vscode-extension && npm run build",
27
- "test": "python3 -m pytest tests/ -v",
28
- "test:unit": "python3 -m pytest tests/unit/ -v",
29
- "test:integration": "python3 -m pytest tests/integration/ -v",
21
+ "frontend:dev": "vite --host 127.0.0.1",
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
+ "build:assets": "vite build && node scripts/build_frontend_assets.mjs",
24
+ "build:python": "node scripts/run_python.mjs -m build",
25
+ "check:python": "node scripts/run_python.mjs scripts/check_python.py",
26
+ "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend",
27
+ "lint:frontend": "node scripts/lint_frontend.mjs",
28
+ "typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
29
+ "typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
30
+ "test": "node scripts/run_python.mjs -m pytest tests/ -v",
31
+ "test:unit": "node scripts/run_python.mjs -m pytest tests/unit/ -v",
32
+ "test:integration": "node scripts/run_python.mjs -m pytest tests/integration/ -v",
30
33
  "test:visual": "playwright test",
31
34
  "capture:workspace": "node scripts/capture/capture_workspace.js",
32
35
  "capture:graph": "node scripts/capture/capture_graph.js",
33
36
  "capture:skills": "node scripts/capture/capture_skills.js",
34
37
  "capture:enterprise": "node scripts/capture/capture_enterprise.js",
35
38
  "capture:onboarding": "node scripts/capture/capture_onboarding.js",
39
+ "desktop:tauri": "tauri dev",
40
+ "desktop:tauri:build": "tauri build",
41
+ "desktop:tauri:check": "cd src-tauri && cargo check",
42
+ "desktop:electron": "electron desktop/electron/main.cjs",
36
43
  "package:vsix": "node scripts/build_vsix.mjs",
37
44
  "release:artifacts": "npm run build:assets && npm run build:python && npm pack && npm run package:vsix",
38
- "release:validate": "python3 scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
45
+ "release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
39
46
  "publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
40
- "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",
47
+ "publish:pypi": "npm run build:python && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
41
48
  "publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",
42
49
  "publish:openvsx": "cd vscode-extension && npm run package:vsix && npm run publish:openvsx",
43
- "publish:all": "npm run release:artifacts && npm run release:validate && npm publish ltcai-$npm_package_version.tgz --access public && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl && cd vscode-extension && npm run publish:vscode && npm run publish:openvsx"
50
+ "publish:all": "npm run release:artifacts && npm run release:validate && npm publish ltcai-$npm_package_version.tgz --access public && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl && cd vscode-extension && npm run publish:vscode && npm run publish:openvsx"
44
51
  },
45
52
  "keywords": [
46
53
  "ltcai",
@@ -74,23 +81,12 @@
74
81
  "mcp_registry.py",
75
82
  "latticeai/**/*.py",
76
83
  "skills/",
77
- "static/account.html",
78
- "static/chat.html",
79
- "static/admin.html",
80
- "static/graph.html",
81
- "static/workspace.html",
82
- "static/plugins.html",
83
- "static/workflows.html",
84
- "static/agents.html",
85
- "static/activity.html",
86
84
  "static/favicon.ico",
87
85
  "static/manifest.json",
88
86
  "static/sw.js",
89
- "static/workspace.css",
90
- "static/platform.css",
91
- "static/scripts/",
92
87
  "static/css/",
93
- "static/v3/",
88
+ "frontend/",
89
+ "static/app/",
94
90
  "static/icons/",
95
91
  "plugins/",
96
92
  "scripts/",
@@ -101,6 +97,12 @@
101
97
  "setup_wizard.py",
102
98
  "knowledge_graph_api.py",
103
99
  "static/vendor/",
100
+ "src-tauri/",
101
+ "!src-tauri/gen/",
102
+ "!src-tauri/gen/**",
103
+ "!src-tauri/target/",
104
+ "!src-tauri/target/**",
105
+ "desktop/electron/",
104
106
  "docs/*.md",
105
107
  "!docs/assets/",
106
108
  "!docs/images/"
@@ -109,6 +111,31 @@
109
111
  "access": "public"
110
112
  },
111
113
  "devDependencies": {
112
- "@playwright/test": "^1.60.0"
114
+ "@playwright/test": "^1.60.0",
115
+ "@tailwindcss/postcss": "^4.3.0",
116
+ "@tanstack/react-query": "^5.101.0",
117
+ "@tauri-apps/api": "^2.0.0",
118
+ "@tauri-apps/cli": "^2.0.0",
119
+ "@types/cytoscape": "^3.21.9",
120
+ "@types/react": "^19.2.17",
121
+ "@types/react-dom": "^19.2.3",
122
+ "@vitejs/plugin-react": "^6.0.2",
123
+ "autoprefixer": "^10.5.0",
124
+ "class-variance-authority": "^0.7.1",
125
+ "clsx": "^2.1.1",
126
+ "cytoscape": "^3.34.0",
127
+ "electron": "^42.4.0",
128
+ "lucide-react": "^1.17.0",
129
+ "openapi-fetch": "^0.17.0",
130
+ "openapi-typescript": "^7.13.0",
131
+ "postcss": "^8.5.15",
132
+ "react": "^19.2.7",
133
+ "react-dom": "^19.2.7",
134
+ "reactflow": "^11.11.4",
135
+ "tailwind-merge": "^3.6.0",
136
+ "tailwindcss": "^4.3.0",
137
+ "typescript": "^5.9.3",
138
+ "vite": "^8.0.16",
139
+ "zustand": "^5.0.14"
113
140
  }
114
141
  }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ const repo = join(import.meta.dirname, "..");
6
+ const appDir = join(repo, "static", "app");
7
+ const nestedViteManifest = join(appDir, ".vite", "asset-manifest.json");
8
+ const publicManifest = join(appDir, "asset-manifest.json");
9
+ const pkg = JSON.parse(readFileSync(join(repo, "package.json"), "utf8"));
10
+
11
+ const viteManifest = existsSync(nestedViteManifest) ? nestedViteManifest : publicManifest;
12
+ if (!existsSync(viteManifest)) {
13
+ console.error("Vite manifest missing. Run `vite build` before build_frontend_assets.mjs.");
14
+ process.exit(1);
15
+ }
16
+
17
+ const raw = JSON.parse(readFileSync(viteManifest, "utf8"));
18
+ const assets = {};
19
+ for (const [key, value] of Object.entries(raw)) {
20
+ if (value && typeof value === "object") {
21
+ if (value.file) assets[key] = `/static/app/${value.file}`;
22
+ for (const css of value.css || []) assets[css] = `/static/app/${css}`;
23
+ for (const asset of value.assets || []) assets[asset] = `/static/app/${asset}`;
24
+ }
25
+ }
26
+
27
+ const manifest = {
28
+ version: pkg.version,
29
+ generated_at: "vite",
30
+ entrypoints: {
31
+ app: assets["frontend/index.html"] || "/static/app/index.html",
32
+ },
33
+ assets,
34
+ vite: raw,
35
+ };
36
+
37
+ writeFileSync(publicManifest, JSON.stringify(manifest, null, 2) + "\n", "utf8");
38
+ console.log(`wrote static/app/asset-manifest.json with ${Object.keys(assets).length} assets`);
@@ -31,7 +31,7 @@ TARGETS = [
31
31
  ("package-lock.json", "package-lock", None),
32
32
  ("vscode-extension/package.json", "json", "version"),
33
33
  ("vscode-extension/package-lock.json", "package-lock", None),
34
- ("static/v3/asset-manifest.json", "json", "version"),
34
+ ("static/app/asset-manifest.json", "json", "version"),
35
35
  ]
36
36
 
37
37
 
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+ """Export the FastAPI OpenAPI schema used by the desktop frontend client."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ REPO = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(REPO))
13
+
14
+
15
+ def main() -> int:
16
+ from latticeai.app_factory import create_app
17
+
18
+ target = Path(sys.argv[1] if len(sys.argv) > 1 else "frontend/openapi.json")
19
+ target.parent.mkdir(parents=True, exist_ok=True)
20
+ os.environ.setdefault("LATTICEAI_REQUIRE_AUTH", "false")
21
+ os.environ.setdefault("LATTICEAI_TUNNEL", "false")
22
+ os.environ.setdefault("LATTICEAI_AUTOLOAD_MODELS", "false")
23
+ app = create_app()
24
+ schema = app.openapi()
25
+ target.write_text(json.dumps(schema, sort_keys=True, indent=2) + "\n", encoding="utf-8")
26
+ print(f"wrote {target} with {len(schema.get('paths', {}))} paths")
27
+ return 0
28
+
29
+
30
+ if __name__ == "__main__":
31
+ raise SystemExit(main())
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const repo = join(import.meta.dirname, "..");
7
+ const frontend = join(repo, "frontend");
8
+ const staticRoot = join(repo, "static");
9
+ let failures = 0;
10
+
11
+ function fail(message) {
12
+ failures += 1;
13
+ console.error(`FAIL ${message}`);
14
+ }
15
+
16
+ function walk(dir, exts) {
17
+ if (!existsSync(dir)) return [];
18
+ const out = [];
19
+ for (const name of readdirSync(dir)) {
20
+ const path = join(dir, name);
21
+ const stat = statSync(path);
22
+ if (stat.isDirectory()) {
23
+ if (name === "node_modules") continue;
24
+ out.push(...walk(path, exts));
25
+ } else if (exts.some((ext) => name.endsWith(ext))) {
26
+ out.push(path);
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+
32
+ const tsc = spawnSync("npx", ["tsc", "-p", "tsconfig.json", "--noEmit"], { cwd: repo, encoding: "utf8" });
33
+ if (tsc.status !== 0) fail(`frontend typecheck\n${tsc.stdout}${tsc.stderr}`);
34
+ else console.log("typecheck: frontend TS passes");
35
+
36
+ const files = [
37
+ ...walk(frontend, [".ts", ".tsx", ".css", ".html"]),
38
+ ...walk(join(staticRoot, "app"), [".js", ".css", ".html", ".json"]),
39
+ join(staticRoot, "sw.js"),
40
+ join(staticRoot, "manifest.json"),
41
+ ].filter(existsSync);
42
+
43
+ const external = /https?:\/\/(fonts\.googleapis\.com|fonts\.gstatic\.com|cdn\.jsdelivr\.net|unpkg\.com|cdnjs\.cloudflare\.com)/;
44
+ const stale = /static\/v3|lint_v3|build_v3_assets/;
45
+ let scanned = 0;
46
+ for (const file of files) {
47
+ scanned += 1;
48
+ const text = readFileSync(file, "utf8");
49
+ if (external.test(text)) fail(`${relative(repo, file)}: CDN reference`);
50
+ if (stale.test(text)) fail(`${relative(repo, file)}: stale v3 frontend reference`);
51
+ }
52
+ console.log(`privacy: ${scanned} frontend/static files scanned`);
53
+
54
+ const client = readFileSync(join(frontend, "src", "api", "client.ts"), "utf8");
55
+ if (!client.includes("openapi-fetch") || !client.includes("./openapi")) {
56
+ fail("frontend/src/api/client.ts must use the generated OpenAPI client");
57
+ }
58
+ if (!existsSync(join(frontend, "src", "api", "openapi.ts"))) {
59
+ fail("generated OpenAPI types missing");
60
+ }
61
+
62
+ const openapi = JSON.parse(readFileSync(join(frontend, "openapi.json"), "utf8"));
63
+ const openapiPaths = Object.keys(openapi.paths || {});
64
+ const requiredPaths = [
65
+ "/health",
66
+ "/chat",
67
+ "/api/graph",
68
+ "/api/search/hybrid",
69
+ "/api/knowledge-graph/export",
70
+ "/api/memory/recall",
71
+ "/agents/api/run",
72
+ "/workflows/api/definitions",
73
+ "/workspace/os",
74
+ "/models",
75
+ ];
76
+ if (openapiPaths.length < 300) fail(`OpenAPI path count too low: ${openapiPaths.length}`);
77
+ for (const path of requiredPaths) {
78
+ if (!openapiPaths.includes(path)) fail(`OpenAPI path missing: ${path}`);
79
+ }
80
+ console.log(`api: generated OpenAPI schema exposes ${openapiPaths.length} paths`);
81
+
82
+ if (failures) {
83
+ console.error(`\nfrontend lint: ${failures} failure(s)`);
84
+ process.exit(1);
85
+ }
86
+ console.log("\nfrontend lint: all checks pass");
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const args = process.argv.slice(2);
7
+
8
+ if (args.length === 0) {
9
+ console.error("usage: node scripts/run_python.mjs <python-args...>");
10
+ process.exit(2);
11
+ }
12
+
13
+ const candidates = [
14
+ process.env.LTCAI_PYTHON,
15
+ path.join(process.cwd(), ".venv", "bin", "python"),
16
+ path.join(process.cwd(), ".venv", "Scripts", "python.exe"),
17
+ "python3",
18
+ "python",
19
+ ].filter(Boolean);
20
+
21
+ let lastError = null;
22
+
23
+ for (const candidate of candidates) {
24
+ const isPath = candidate.includes(path.sep);
25
+ if (isPath && !existsSync(candidate)) {
26
+ continue;
27
+ }
28
+
29
+ const result = spawnSync(candidate, args, {
30
+ cwd: process.cwd(),
31
+ env: process.env,
32
+ stdio: "inherit",
33
+ });
34
+
35
+ if (result.error) {
36
+ if (result.error.code === "ENOENT") {
37
+ lastError = result.error;
38
+ continue;
39
+ }
40
+ throw result.error;
41
+ }
42
+
43
+ process.exit(result.status ?? 1);
44
+ }
45
+
46
+ console.error(`No usable Python executable found. Last error: ${lastError?.message ?? "none"}`);
47
+ process.exit(127);