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.
- package/README.md +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +46 -0
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +355 -952
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +70 -29
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +5 -14
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +2 -21
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +268 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +7 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1304
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/Act-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
package/local_knowledge_api.py
CHANGED
|
@@ -1,345 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Compatibility shim for :mod:`latticeai.services.local_knowledge`."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import threading
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any, Callable, Dict, Optional
|
|
3
|
+
import sys
|
|
7
4
|
|
|
8
|
-
from
|
|
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": "
|
|
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
|
{
|
package/scripts/bump_version.py
CHANGED
|
@@ -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
|
+
}
|