ltcai 4.2.0 → 4.3.1
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 +28 -21
- package/bin/ltcai.js +6 -2
- package/docs/CHANGELOG.md +72 -0
- package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +69 -0
- package/docs/V4_3_PRIVACY_AUDIT.md +60 -0
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +53 -0
- package/docs/V4_3_VALIDATION_REPORT.md +58 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +19 -25
- package/frontend/openapi.json +213 -1
- package/frontend/src/App.tsx +15 -1
- package/frontend/src/api/client.ts +26 -1
- package/frontend/src/api/openapi.ts +268 -0
- package/frontend/src/pages/Act.tsx +63 -2
- package/frontend/src/pages/Library.tsx +9 -3
- package/frontend/src/pages/System.tsx +58 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +360 -47
- package/lattice_brain/storage/sqlite.py +15 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +11 -0
- package/latticeai/api/agents.py +3 -1
- package/latticeai/api/models.py +66 -18
- package/latticeai/api/portability.py +59 -2
- package/latticeai/app_factory.py +9 -0
- package/latticeai/brain/projection.py +12 -2
- package/latticeai/brain/retrieval.py +10 -0
- package/latticeai/brain/store.py +6 -1
- package/latticeai/core/config.py +4 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/product_hardening.py +218 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +52 -12
- package/latticeai/services/kg_portability.py +147 -4
- package/latticeai/services/model_runtime.py +83 -2
- package/ltcai_cli.py +16 -4
- package/package.json +5 -4
- package/requirements.txt +17 -0
- package/scripts/clean_release_artifacts.mjs +27 -0
- package/scripts/lint_frontend.mjs +5 -0
- package/scripts/validate_release_artifacts.py +10 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +356 -24
- package/src-tauri/tauri.conf.json +20 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/{index-C_HAkbAg.js → index-BhPuj8rT.js} +45 -45
- package/static/app/assets/index-BhPuj8rT.js.map +1 -0
- package/static/app/assets/{index-CDjiH_se.css → index-yZswHE3d.css} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/index-C_HAkbAg.js.map +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const repo = join(import.meta.dirname, "..");
|
|
6
|
+
const version = process.argv[2] || process.env.npm_package_version;
|
|
7
|
+
|
|
8
|
+
if (!version || !/^\d+\.\d+\.\d+([.-][0-9A-Za-z.]+)?$/.test(version)) {
|
|
9
|
+
console.error("usage: node scripts/clean_release_artifacts.mjs <version>");
|
|
10
|
+
process.exit(2);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const targets = [
|
|
14
|
+
join(repo, "dist", `ltcai-${version}-py3-none-any.whl`),
|
|
15
|
+
join(repo, "dist", `ltcai-${version}.tar.gz`),
|
|
16
|
+
join(repo, "dist", `ltcai-${version}.vsix`),
|
|
17
|
+
join(repo, `ltcai-${version}.tgz`),
|
|
18
|
+
join(repo, "src-tauri", "target", "release", "bundle", "dmg", `Lattice AI_${version}_aarch64.dmg`),
|
|
19
|
+
join(repo, "src-tauri", "target", "release", "bundle", "macos", "Lattice AI.app"),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
if (existsSync(target)) {
|
|
24
|
+
rmSync(target, { recursive: true, force: true });
|
|
25
|
+
console.log(`removed ${target}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -76,7 +76,12 @@ const requiredPaths = [
|
|
|
76
76
|
"/api/brain/storage/postgres/docker",
|
|
77
77
|
"/api/brain/storage/migrate-postgres",
|
|
78
78
|
"/api/knowledge-graph/archive",
|
|
79
|
+
"/api/knowledge-graph/archive/inspect",
|
|
80
|
+
"/api/knowledge-graph/archive/verify",
|
|
81
|
+
"/api/knowledge-graph/archive/import",
|
|
79
82
|
"/api/knowledge-graph/archive/restore",
|
|
83
|
+
"/api/knowledge-graph/backup-health",
|
|
84
|
+
"/admin/product-hardening",
|
|
80
85
|
];
|
|
81
86
|
if (openapiPaths.length < 300) fail(`OpenAPI path count too low: ${openapiPaths.length}`);
|
|
82
87
|
for (const path of requiredPaths) {
|
|
@@ -10,6 +10,7 @@ effort) that the VSIX actually contains the compiled extension entrypoint.
|
|
|
10
10
|
Usage:
|
|
11
11
|
python scripts/validate_release_artifacts.py 1.1.0
|
|
12
12
|
python scripts/validate_release_artifacts.py 1.1.0 --require-vsix
|
|
13
|
+
python scripts/validate_release_artifacts.py 1.1.0 --require-dmg
|
|
13
14
|
python scripts/validate_release_artifacts.py 1.1.0 --dist dist --json
|
|
14
15
|
|
|
15
16
|
Exit code is non-zero on any failure so CI can fail fast.
|
|
@@ -63,6 +64,7 @@ def validate(
|
|
|
63
64
|
*,
|
|
64
65
|
require_vsix: bool,
|
|
65
66
|
require_tgz: bool,
|
|
67
|
+
require_dmg: bool = False,
|
|
66
68
|
) -> Dict[str, object]:
|
|
67
69
|
errors: List[str] = []
|
|
68
70
|
warnings: List[str] = []
|
|
@@ -105,6 +107,12 @@ def validate(
|
|
|
105
107
|
else:
|
|
106
108
|
warnings.append(f"npm tarball not found: {tgz.name} (run `npm pack`)")
|
|
107
109
|
|
|
110
|
+
dmg = dist_dir.parent / "src-tauri" / "target" / "release" / "bundle" / "dmg" / f"Lattice AI_{version}_aarch64.dmg"
|
|
111
|
+
if dmg.is_file():
|
|
112
|
+
found["dmg"] = str(dmg)
|
|
113
|
+
elif require_dmg:
|
|
114
|
+
errors.append(f"missing dmg: {dmg}")
|
|
115
|
+
|
|
108
116
|
# Guard against stale-version mixing: warn loudly about other-version builds
|
|
109
117
|
# so a `dist/*` glob upload is obviously unsafe.
|
|
110
118
|
other_versions = set()
|
|
@@ -137,6 +145,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
137
145
|
parser.add_argument("--dist", default="dist", help="dist directory (default: dist)")
|
|
138
146
|
parser.add_argument("--require-vsix", action="store_true", help="fail if the VSIX is absent")
|
|
139
147
|
parser.add_argument("--require-tgz", action="store_true", help="check for npm pack tarball at repo root")
|
|
148
|
+
parser.add_argument("--require-dmg", action="store_true", help="fail if the Tauri DMG is absent")
|
|
140
149
|
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
141
150
|
args = parser.parse_args(argv)
|
|
142
151
|
|
|
@@ -145,6 +154,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
145
154
|
Path(args.dist),
|
|
146
155
|
require_vsix=args.require_vsix,
|
|
147
156
|
require_tgz=args.require_tgz,
|
|
157
|
+
require_dmg=args.require_dmg,
|
|
148
158
|
)
|
|
149
159
|
|
|
150
160
|
if args.json:
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
package/src-tauri/src/main.rs
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
use std::{
|
|
2
2
|
env,
|
|
3
|
+
fs::OpenOptions,
|
|
4
|
+
path::PathBuf,
|
|
3
5
|
process::{Child, Command, Stdio},
|
|
4
6
|
sync::Mutex,
|
|
5
7
|
};
|
|
6
8
|
|
|
9
|
+
use serde::Serialize;
|
|
7
10
|
use tauri::{Manager, State};
|
|
8
11
|
|
|
9
12
|
struct BackendState {
|
|
10
13
|
origin: String,
|
|
14
|
+
command: String,
|
|
15
|
+
cwd: Option<String>,
|
|
11
16
|
child: Mutex<Option<Child>>,
|
|
17
|
+
last_error: Mutex<Option<String>>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Serialize)]
|
|
21
|
+
struct BackendStatus {
|
|
22
|
+
origin: String,
|
|
23
|
+
command: String,
|
|
24
|
+
cwd: Option<String>,
|
|
25
|
+
running: bool,
|
|
26
|
+
pid: Option<u32>,
|
|
27
|
+
last_error: Option<String>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
struct BackendLaunch {
|
|
31
|
+
command: String,
|
|
32
|
+
program: String,
|
|
33
|
+
args: Vec<String>,
|
|
34
|
+
cwd: Option<PathBuf>,
|
|
12
35
|
}
|
|
13
36
|
|
|
14
37
|
#[tauri::command]
|
|
@@ -16,60 +39,369 @@ fn backend_origin(state: State<'_, BackendState>) -> String {
|
|
|
16
39
|
state.origin.clone()
|
|
17
40
|
}
|
|
18
41
|
|
|
42
|
+
#[tauri::command]
|
|
43
|
+
fn backend_status(state: State<'_, BackendState>) -> BackendStatus {
|
|
44
|
+
status_from_state(&state)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[tauri::command]
|
|
48
|
+
fn restart_backend(state: State<'_, BackendState>) -> BackendStatus {
|
|
49
|
+
kill_backend(&state);
|
|
50
|
+
let launch = backend_launch(&state.origin);
|
|
51
|
+
match spawn_backend(&state.origin, &launch) {
|
|
52
|
+
Ok(child) => {
|
|
53
|
+
if let Ok(mut slot) = state.child.lock() {
|
|
54
|
+
*slot = child;
|
|
55
|
+
}
|
|
56
|
+
set_error(&state, None);
|
|
57
|
+
}
|
|
58
|
+
Err(err) => set_error(&state, Some(err)),
|
|
59
|
+
}
|
|
60
|
+
status_from_state(&state)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[tauri::command]
|
|
64
|
+
fn shutdown_backend(state: State<'_, BackendState>) -> BackendStatus {
|
|
65
|
+
kill_backend(&state);
|
|
66
|
+
status_from_state(&state)
|
|
67
|
+
}
|
|
68
|
+
|
|
19
69
|
fn split_command(command: &str) -> Vec<String> {
|
|
20
|
-
command
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
70
|
+
command.split_whitespace().map(|part| part.to_string()).collect()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn command_in_path(name: &str) -> Option<String> {
|
|
74
|
+
let mut dirs: Vec<PathBuf> = env::var_os("PATH")
|
|
75
|
+
.map(|value| env::split_paths(&value).collect())
|
|
76
|
+
.unwrap_or_default();
|
|
77
|
+
dirs.extend([
|
|
78
|
+
PathBuf::from("/opt/homebrew/bin"),
|
|
79
|
+
PathBuf::from("/usr/local/bin"),
|
|
80
|
+
PathBuf::from("/usr/bin"),
|
|
81
|
+
PathBuf::from("/bin"),
|
|
82
|
+
]);
|
|
83
|
+
for dir in dirs {
|
|
84
|
+
let candidate = dir.join(name);
|
|
85
|
+
if candidate.is_file() {
|
|
86
|
+
return Some(candidate.to_string_lossy().to_string());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
None
|
|
24
90
|
}
|
|
25
91
|
|
|
26
|
-
fn
|
|
92
|
+
fn python_candidates() -> Vec<String> {
|
|
93
|
+
let mut out = Vec::new();
|
|
94
|
+
if let Ok(value) = env::var("LTCAI_PYTHON") {
|
|
95
|
+
out.push(value);
|
|
96
|
+
}
|
|
97
|
+
for name in ["python3", "python"] {
|
|
98
|
+
if let Some(path) = command_in_path(name) {
|
|
99
|
+
out.push(path);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
out.extend([
|
|
103
|
+
"/opt/homebrew/bin/python3".to_string(),
|
|
104
|
+
"/usr/local/bin/python3".to_string(),
|
|
105
|
+
"/usr/bin/python3".to_string(),
|
|
106
|
+
]);
|
|
107
|
+
out.sort();
|
|
108
|
+
out.dedup();
|
|
109
|
+
out
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn module_importable(python: &str, module: &str) -> bool {
|
|
113
|
+
Command::new(python)
|
|
114
|
+
.args(["-c", &format!("import {module}")])
|
|
115
|
+
.stdout(Stdio::null())
|
|
116
|
+
.stderr(Stdio::null())
|
|
117
|
+
.status()
|
|
118
|
+
.map(|status| status.success())
|
|
119
|
+
.unwrap_or(false)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn resource_dir() -> Option<PathBuf> {
|
|
123
|
+
let exe = env::current_exe().ok()?;
|
|
124
|
+
let macos_dir = exe.parent()?;
|
|
125
|
+
let contents_dir = macos_dir.parent()?;
|
|
126
|
+
let resources = contents_dir.join("Resources");
|
|
127
|
+
if resources.exists() {
|
|
128
|
+
Some(resources)
|
|
129
|
+
} else {
|
|
130
|
+
None
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn bundled_python_root() -> Option<PathBuf> {
|
|
135
|
+
let resources = resource_dir()?;
|
|
136
|
+
let up = resources.join("_up_");
|
|
137
|
+
if up.join("ltcai_cli.py").is_file() {
|
|
138
|
+
Some(up)
|
|
139
|
+
} else if resources.join("ltcai_cli.py").is_file() {
|
|
140
|
+
Some(resources)
|
|
141
|
+
} else {
|
|
142
|
+
None
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn desktop_runtime_dir() -> Option<PathBuf> {
|
|
147
|
+
let home = env::var("HOME").ok()?;
|
|
148
|
+
let dir = PathBuf::from(home).join(".ltcai").join("desktop-runtime");
|
|
149
|
+
let _ = std::fs::create_dir_all(&dir);
|
|
150
|
+
Some(dir)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn python_path_env(launch: &BackendLaunch) -> Option<String> {
|
|
154
|
+
let mut paths: Vec<PathBuf> = Vec::new();
|
|
155
|
+
if let Some(resources) = bundled_python_root() {
|
|
156
|
+
paths.push(resources);
|
|
157
|
+
}
|
|
158
|
+
if let Some(cwd) = &launch.cwd {
|
|
159
|
+
if !paths.iter().any(|path| path == cwd) {
|
|
160
|
+
paths.push(cwd.clone());
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if let Some(existing) = env::var_os("PYTHONPATH") {
|
|
164
|
+
paths.extend(env::split_paths(&existing));
|
|
165
|
+
}
|
|
166
|
+
env::join_paths(paths).ok().map(|value| value.to_string_lossy().to_string())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn backend_launch(origin: &str) -> BackendLaunch {
|
|
170
|
+
let port = origin.rsplit(':').next().unwrap_or("8765").to_string();
|
|
171
|
+
if let Ok(command) = env::var("LATTICEAI_DESKTOP_BACKEND_CMD") {
|
|
172
|
+
let parts = split_command(&command);
|
|
173
|
+
if let Some(program) = parts.first() {
|
|
174
|
+
return BackendLaunch {
|
|
175
|
+
command,
|
|
176
|
+
program: program.clone(),
|
|
177
|
+
args: parts[1..].to_vec(),
|
|
178
|
+
cwd: env::var("LATTICEAI_DESKTOP_BACKEND_CWD").ok().map(PathBuf::from),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for name in ["LTCAI", "ltcai"] {
|
|
184
|
+
if let Some(program) = command_in_path(name) {
|
|
185
|
+
return BackendLaunch {
|
|
186
|
+
command: format!("{program} --host 127.0.0.1 --port {port}"),
|
|
187
|
+
program,
|
|
188
|
+
args: vec!["--host".into(), "127.0.0.1".into(), "--port".into(), port],
|
|
189
|
+
cwd: None,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for python in python_candidates() {
|
|
195
|
+
if module_importable(&python, "ltcai_cli") {
|
|
196
|
+
return BackendLaunch {
|
|
197
|
+
command: format!("{python} -m ltcai_cli --host 127.0.0.1 --port {port}"),
|
|
198
|
+
program: python,
|
|
199
|
+
args: vec![
|
|
200
|
+
"-m".into(),
|
|
201
|
+
"ltcai_cli".into(),
|
|
202
|
+
"--host".into(),
|
|
203
|
+
"127.0.0.1".into(),
|
|
204
|
+
"--port".into(),
|
|
205
|
+
port,
|
|
206
|
+
],
|
|
207
|
+
cwd: None,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if let Some(resources) = bundled_python_root() {
|
|
213
|
+
let launcher = resources.join("ltcai_cli.py");
|
|
214
|
+
if launcher.is_file() {
|
|
215
|
+
if let Some(python) = python_candidates().into_iter().next() {
|
|
216
|
+
return BackendLaunch {
|
|
217
|
+
command: format!("{python} {} --host 127.0.0.1 --port {port}", launcher.display()),
|
|
218
|
+
program: python,
|
|
219
|
+
args: vec![
|
|
220
|
+
launcher.to_string_lossy().to_string(),
|
|
221
|
+
"--host".into(),
|
|
222
|
+
"127.0.0.1".into(),
|
|
223
|
+
"--port".into(),
|
|
224
|
+
port,
|
|
225
|
+
],
|
|
226
|
+
cwd: None,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
BackendLaunch {
|
|
233
|
+
command: "unavailable: LTCAI executable or importable ltcai_cli module not found".to_string(),
|
|
234
|
+
program: String::new(),
|
|
235
|
+
args: Vec::new(),
|
|
236
|
+
cwd: None,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn set_error(state: &BackendState, err: Option<String>) {
|
|
241
|
+
if let Ok(mut last) = state.last_error.lock() {
|
|
242
|
+
*last = err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fn spawn_backend(origin: &str, launch: &BackendLaunch) -> Result<Option<Child>, String> {
|
|
27
247
|
if env::var("LATTICEAI_DESKTOP_NO_BACKEND").is_ok() {
|
|
28
|
-
return None;
|
|
248
|
+
return Ok(None);
|
|
29
249
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
let parts = split_command(&command);
|
|
33
|
-
if parts.is_empty() {
|
|
34
|
-
return None;
|
|
250
|
+
if launch.program.is_empty() {
|
|
251
|
+
return Err("Desktop backend unavailable: LTCAI executable or importable ltcai_cli module not found.".to_string());
|
|
35
252
|
}
|
|
36
|
-
|
|
37
|
-
cmd
|
|
253
|
+
|
|
254
|
+
let mut cmd = Command::new(&launch.program);
|
|
255
|
+
cmd.args(&launch.args)
|
|
38
256
|
.env("LATTICEAI_HOST", "127.0.0.1")
|
|
39
257
|
.env("LATTICEAI_PORT", origin.rsplit(':').next().unwrap_or("8765"))
|
|
258
|
+
.env("LATTICEAI_ENABLE_TELEGRAM", "false")
|
|
259
|
+
.env("LATTICEAI_AUTOLOAD_MODELS", "false")
|
|
260
|
+
.env("LATTICEAI_ALLOW_MODEL_DOWNLOADS", "false")
|
|
261
|
+
.env("LATTICEAI_CORS_ALLOW_NETWORK", "false")
|
|
262
|
+
.env("LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", "false")
|
|
40
263
|
.env("LATTICEAI_TUNNEL", "false")
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
264
|
+
.env(
|
|
265
|
+
"PATH",
|
|
266
|
+
format!(
|
|
267
|
+
"{}:{}",
|
|
268
|
+
"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
269
|
+
env::var("PATH").unwrap_or_default()
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
if let Some(runtime_dir) = desktop_runtime_dir() {
|
|
273
|
+
cmd.env("LATTICEAI_AGENT_ROOT", runtime_dir.join("agent_workspace"));
|
|
274
|
+
if launch.cwd.is_none() {
|
|
275
|
+
cmd.current_dir(&runtime_dir);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if let Some(python_path) = python_path_env(launch) {
|
|
279
|
+
cmd.env("PYTHONPATH", python_path);
|
|
280
|
+
}
|
|
281
|
+
if let Some(cwd) = &launch.cwd {
|
|
44
282
|
cmd.current_dir(cwd);
|
|
45
283
|
}
|
|
46
|
-
|
|
284
|
+
if let Ok(home) = env::var("HOME") {
|
|
285
|
+
let log_dir = PathBuf::from(home).join(".ltcai");
|
|
286
|
+
let _ = std::fs::create_dir_all(&log_dir);
|
|
287
|
+
if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_dir.join("desktop-sidecar.log")) {
|
|
288
|
+
cmd.stdout(Stdio::from(file));
|
|
289
|
+
} else {
|
|
290
|
+
cmd.stdout(Stdio::null());
|
|
291
|
+
}
|
|
292
|
+
if let Ok(file) = OpenOptions::new().create(true).append(true).open(log_dir.join("desktop-sidecar.err.log")) {
|
|
293
|
+
cmd.stderr(Stdio::from(file));
|
|
294
|
+
} else {
|
|
295
|
+
cmd.stderr(Stdio::null());
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
cmd.spawn()
|
|
302
|
+
.map(Some)
|
|
303
|
+
.map_err(|err| format!("Failed to start desktop backend '{}': {}", launch.command, err))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fn kill_backend(state: &BackendState) {
|
|
307
|
+
if let Ok(mut child) = state.child.lock() {
|
|
308
|
+
if let Some(mut process) = child.take() {
|
|
309
|
+
let _ = process.kill();
|
|
310
|
+
let _ = process.wait();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn status_from_state(state: &BackendState) -> BackendStatus {
|
|
316
|
+
let mut running = false;
|
|
317
|
+
let mut pid = None;
|
|
318
|
+
if let Ok(mut child_slot) = state.child.lock() {
|
|
319
|
+
if let Some(child) = child_slot.as_mut() {
|
|
320
|
+
match child.try_wait() {
|
|
321
|
+
Ok(Some(status)) => {
|
|
322
|
+
set_error(state, Some(format!("Desktop backend exited with status {}", status)));
|
|
323
|
+
*child_slot = None;
|
|
324
|
+
}
|
|
325
|
+
Ok(None) => {
|
|
326
|
+
running = true;
|
|
327
|
+
pid = Some(child.id());
|
|
328
|
+
}
|
|
329
|
+
Err(err) => set_error(state, Some(format!("Unable to inspect desktop backend: {}", err))),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
let last_error = state.last_error.lock().ok().and_then(|guard| guard.clone());
|
|
334
|
+
BackendStatus {
|
|
335
|
+
origin: state.origin.clone(),
|
|
336
|
+
command: state.command.clone(),
|
|
337
|
+
cwd: state.cwd.clone(),
|
|
338
|
+
running,
|
|
339
|
+
pid,
|
|
340
|
+
last_error,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn wait_for_backend(origin: &str) {
|
|
345
|
+
let host_port = origin
|
|
346
|
+
.trim_start_matches("http://")
|
|
347
|
+
.trim_start_matches("https://")
|
|
348
|
+
.split('/')
|
|
349
|
+
.next()
|
|
350
|
+
.unwrap_or("127.0.0.1:8765")
|
|
351
|
+
.to_string();
|
|
352
|
+
for _ in 0..45 {
|
|
353
|
+
if std::net::TcpStream::connect(&host_port).is_ok() {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
357
|
+
}
|
|
47
358
|
}
|
|
48
359
|
|
|
49
360
|
fn main() {
|
|
50
361
|
let origin = env::var("LATTICEAI_DESKTOP_BACKEND_ORIGIN")
|
|
51
362
|
.unwrap_or_else(|_| "http://127.0.0.1:8765".to_string());
|
|
52
|
-
let
|
|
363
|
+
let launch = backend_launch(&origin);
|
|
364
|
+
let command = launch.command.clone();
|
|
365
|
+
let cwd = launch.cwd.as_ref().map(|path| path.to_string_lossy().to_string());
|
|
366
|
+
let (child, last_error) = match spawn_backend(&origin, &launch) {
|
|
367
|
+
Ok(child) => (child, None),
|
|
368
|
+
Err(err) => (None, Some(err)),
|
|
369
|
+
};
|
|
53
370
|
tauri::Builder::default()
|
|
54
371
|
.manage(BackendState {
|
|
55
372
|
origin,
|
|
373
|
+
command,
|
|
374
|
+
cwd,
|
|
56
375
|
child: Mutex::new(child),
|
|
376
|
+
last_error: Mutex::new(last_error),
|
|
57
377
|
})
|
|
58
|
-
.invoke_handler(tauri::generate_handler![
|
|
378
|
+
.invoke_handler(tauri::generate_handler![
|
|
379
|
+
backend_origin,
|
|
380
|
+
backend_status,
|
|
381
|
+
restart_backend,
|
|
382
|
+
shutdown_backend
|
|
383
|
+
])
|
|
59
384
|
.setup(|app| {
|
|
60
385
|
if let Some(window) = app.get_webview_window("main") {
|
|
61
386
|
let _ = window.set_title("Lattice AI");
|
|
387
|
+
let _ = window.show();
|
|
388
|
+
let _ = window.set_focus();
|
|
389
|
+
let origin = app.state::<BackendState>().origin.clone();
|
|
390
|
+
let target = format!("{}/app", origin.trim_end_matches('/'));
|
|
391
|
+
let mut window_for_nav = window.clone();
|
|
392
|
+
std::thread::spawn(move || {
|
|
393
|
+
wait_for_backend(&origin);
|
|
394
|
+
if let Ok(url) = tauri::Url::parse(&target) {
|
|
395
|
+
let _ = window_for_nav.navigate(url);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
62
398
|
}
|
|
63
399
|
Ok(())
|
|
64
400
|
})
|
|
65
401
|
.on_window_event(|window, event| {
|
|
66
402
|
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
|
|
67
403
|
if let Some(state) = window.try_state::<BackendState>() {
|
|
68
|
-
|
|
69
|
-
if let Some(process) = child.as_mut() {
|
|
70
|
-
let _ = process.kill();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
404
|
+
kill_backend(&state);
|
|
73
405
|
}
|
|
74
406
|
}
|
|
75
407
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://schema.tauri.app/config/2",
|
|
3
3
|
"productName": "Lattice AI",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.1",
|
|
5
5
|
"identifier": "ai.lattice.desktop",
|
|
6
6
|
"build": {
|
|
7
7
|
"beforeDevCommand": "npm run frontend:dev",
|
|
@@ -31,6 +31,25 @@
|
|
|
31
31
|
"dmg",
|
|
32
32
|
"app"
|
|
33
33
|
],
|
|
34
|
+
"resources": [
|
|
35
|
+
"../auto_setup.py",
|
|
36
|
+
"../kg_schema.py",
|
|
37
|
+
"../knowledge_graph.py",
|
|
38
|
+
"../knowledge_graph_api.py",
|
|
39
|
+
"../llm_router.py",
|
|
40
|
+
"../local_knowledge_api.py",
|
|
41
|
+
"../ltcai_cli.py",
|
|
42
|
+
"../mcp_registry.py",
|
|
43
|
+
"../p_reinforce.py",
|
|
44
|
+
"../server.py",
|
|
45
|
+
"../setup_wizard.py",
|
|
46
|
+
"../telegram_bot.py",
|
|
47
|
+
"../requirements.txt",
|
|
48
|
+
"../latticeai",
|
|
49
|
+
"../lattice_brain",
|
|
50
|
+
"../tools",
|
|
51
|
+
"../static"
|
|
52
|
+
],
|
|
34
53
|
"icon": [
|
|
35
54
|
"../static/icons/icon-192.png",
|
|
36
55
|
"../static/icons/icon-512.png"
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.3.1",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
5
|
"app": "/static/app/index.html"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"index.html": "/static/app/assets/index-
|
|
10
|
-
"assets/index-
|
|
9
|
+
"index.html": "/static/app/assets/index-BhPuj8rT.js",
|
|
10
|
+
"assets/index-yZswHE3d.css": "/static/app/assets/index-yZswHE3d.css"
|
|
11
11
|
},
|
|
12
12
|
"vite": {
|
|
13
13
|
"../node_modules/@tauri-apps/api/core.js": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"isDynamicEntry": true
|
|
18
18
|
},
|
|
19
19
|
"index.html": {
|
|
20
|
-
"file": "assets/index-
|
|
20
|
+
"file": "assets/index-BhPuj8rT.js",
|
|
21
21
|
"name": "index",
|
|
22
22
|
"src": "index.html",
|
|
23
23
|
"isEntry": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"../node_modules/@tauri-apps/api/core.js"
|
|
26
26
|
],
|
|
27
27
|
"css": [
|
|
28
|
-
"assets/index-
|
|
28
|
+
"assets/index-yZswHE3d.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|