ltcai 4.3.0 → 4.3.3

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 (71) hide show
  1. package/README.md +186 -276
  2. package/bin/ltcai.js +6 -2
  3. package/docs/CHANGELOG.md +124 -3
  4. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
  5. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
  6. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
  7. package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
  8. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
  9. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
  10. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
  11. package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
  12. package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
  13. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -25
  14. package/frontend/openapi.json +11 -1
  15. package/frontend/src/App.tsx +15 -1
  16. package/frontend/src/api/client.ts +19 -1
  17. package/frontend/src/api/openapi.ts +10 -0
  18. package/frontend/src/components/primitives.tsx +92 -10
  19. package/frontend/src/pages/Act.tsx +72 -9
  20. package/frontend/src/pages/Ask.tsx +2 -2
  21. package/frontend/src/pages/Brain.tsx +607 -65
  22. package/frontend/src/pages/Capture.tsx +11 -7
  23. package/frontend/src/pages/Library.tsx +12 -6
  24. package/frontend/src/pages/System.tsx +186 -23
  25. package/lattice_brain/__init__.py +1 -1
  26. package/lattice_brain/archive.py +3 -3
  27. package/lattice_brain/storage/sqlite.py +15 -2
  28. package/latticeai/__init__.py +1 -1
  29. package/latticeai/api/agents.py +3 -1
  30. package/latticeai/api/models.py +66 -18
  31. package/latticeai/brain/projection.py +12 -2
  32. package/latticeai/brain/retrieval.py +10 -0
  33. package/latticeai/brain/store.py +6 -1
  34. package/latticeai/core/config.py +3 -1
  35. package/latticeai/core/marketplace.py +1 -1
  36. package/latticeai/core/multi_agent.py +1 -1
  37. package/latticeai/core/product_hardening.py +2 -1
  38. package/latticeai/core/workspace_os.py +1 -1
  39. package/latticeai/services/agent_runtime.py +52 -12
  40. package/latticeai/services/model_runtime.py +83 -2
  41. package/ltcai_cli.py +14 -3
  42. package/package.json +5 -7
  43. package/requirements.txt +17 -0
  44. package/scripts/build_vercel_static.mjs +77 -0
  45. package/scripts/check_markdown_links.mjs +75 -0
  46. package/src-tauri/Cargo.lock +1 -1
  47. package/src-tauri/Cargo.toml +1 -1
  48. package/src-tauri/src/main.rs +269 -27
  49. package/src-tauri/tauri.conf.json +20 -1
  50. package/static/app/asset-manifest.json +5 -5
  51. package/static/app/assets/index-CHHal8Zl.css +2 -0
  52. package/static/app/assets/index-pdzil9ac.js +333 -0
  53. package/static/app/assets/index-pdzil9ac.js.map +1 -0
  54. package/static/app/index.html +2 -2
  55. package/latticeai/api/deps.py +0 -15
  56. package/scripts/capture/README.md +0 -28
  57. package/scripts/capture/capture_enterprise.js +0 -8
  58. package/scripts/capture/capture_graph.js +0 -8
  59. package/scripts/capture/capture_onboarding.js +0 -8
  60. package/scripts/capture/capture_page.js +0 -43
  61. package/scripts/capture/capture_release_media.js +0 -125
  62. package/scripts/capture/capture_skills.js +0 -8
  63. package/scripts/capture/capture_v340.js +0 -88
  64. package/scripts/capture/capture_workspace.js +0 -8
  65. package/scripts/generate_diagrams.py +0 -512
  66. package/scripts/release-0.3.1.sh +0 -105
  67. package/scripts/take_screenshots.js +0 -69
  68. package/static/app/assets/index-RiJTJliG.js +0 -333
  69. package/static/app/assets/index-RiJTJliG.js.map +0 -1
  70. package/static/app/assets/index-yZswHE3d.css +0 -2
  71. package/static/css/tokens.3ba22e37.css +0 -260
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "4.3.0",
4
- "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
3
+ "version": "4.3.3",
4
+ "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, portable encrypted brain archives)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,17 +25,14 @@
25
25
  "check:python": "node scripts/run_python.mjs scripts/check_python.py",
26
26
  "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend",
27
27
  "lint:frontend": "node scripts/lint_frontend.mjs",
28
+ "docs:check-links": "node scripts/check_markdown_links.mjs",
28
29
  "typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
29
30
  "typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
30
31
  "test": "node scripts/run_python.mjs -m pytest tests/ -v",
31
32
  "test:unit": "node scripts/run_python.mjs -m pytest tests/unit/ -v",
32
33
  "test:integration": "node scripts/run_python.mjs -m pytest tests/integration/ -v",
33
34
  "test:visual": "playwright test",
34
- "capture:workspace": "node scripts/capture/capture_workspace.js",
35
- "capture:graph": "node scripts/capture/capture_graph.js",
36
- "capture:skills": "node scripts/capture/capture_skills.js",
37
- "capture:enterprise": "node scripts/capture/capture_enterprise.js",
38
- "capture:onboarding": "node scripts/capture/capture_onboarding.js",
35
+ "vercel:build": "node scripts/build_vercel_static.mjs",
39
36
  "desktop:tauri": "tauri dev",
40
37
  "desktop:tauri:build": "tauri build",
41
38
  "desktop:tauri:check": "cd src-tauri && cargo check",
@@ -68,6 +65,7 @@
68
65
  "files": [
69
66
  "bin/ltcai.js",
70
67
  "LICENSE",
68
+ "requirements.txt",
71
69
  "ltcai_cli.py",
72
70
  "auto_setup.py",
73
71
  "server.py",
@@ -0,0 +1,17 @@
1
+ fastapi>=0.110,<1
2
+ uvicorn>=0.29,<1
3
+ pydantic>=2.7,<3
4
+ httpx>=0.27,<1
5
+ pillow>=10,<13
6
+ openai>=1.30,<3
7
+ python-docx>=1.1,<2
8
+ openpyxl>=3.1,<4
9
+ python-pptx>=0.6.23,<2
10
+ python-multipart>=0.0.9,<0.1
11
+ keyring>=24,<26
12
+ authlib>=1.3,<2
13
+ cryptography>=42,<49
14
+ pdfplumber>=0.11,<0.12
15
+ pypdfium2>=4.30,<6
16
+ watchdog>=4,<7
17
+ psycopg[binary]>=3.2,<4
@@ -0,0 +1,77 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
5
+ const outDir = new URL("../vercel-static/", import.meta.url);
6
+
7
+ await mkdir(outDir, { recursive: true });
8
+
9
+ const html = `<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14
+ <title>Lattice AI ${pkg.version}</title>
15
+ <style>
16
+ :root {
17
+ color-scheme: dark;
18
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
19
+ background: #0f141b;
20
+ color: #e5edf4;
21
+ }
22
+ body {
23
+ margin: 0;
24
+ min-height: 100vh;
25
+ display: grid;
26
+ place-items: center;
27
+ padding: 32px;
28
+ }
29
+ main {
30
+ max-width: 760px;
31
+ border: 1px solid #2d3745;
32
+ border-radius: 8px;
33
+ padding: 32px;
34
+ background: #151b24;
35
+ }
36
+ h1 {
37
+ margin: 0 0 12px;
38
+ font-size: 32px;
39
+ }
40
+ p {
41
+ line-height: 1.6;
42
+ color: #b8c4d2;
43
+ }
44
+ a {
45
+ color: #41ddd2;
46
+ }
47
+ code {
48
+ background: #0f141b;
49
+ border: 1px solid #2d3745;
50
+ border-radius: 4px;
51
+ padding: 2px 6px;
52
+ }
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <main>
57
+ <h1>Lattice AI ${pkg.version}</h1>
58
+ <p>
59
+ Lattice AI is a local-first desktop Digital Brain. The product runtime is the
60
+ Tauri desktop app plus a localhost FastAPI sidecar; it is not hosted on Vercel.
61
+ </p>
62
+ <p>
63
+ This Vercel build is intentionally documentation-only so Git integration checks
64
+ do not try to deploy the desktop runtime or a fake cloud app.
65
+ </p>
66
+ <p>
67
+ Use the validated desktop/package artifacts from the GitHub release process.
68
+ Repository: <a href="${pkg.homepage}">${pkg.homepage}</a>
69
+ </p>
70
+ <p>Runtime route when installed locally: <code>http://127.0.0.1:4825/app</code></p>
71
+ </main>
72
+ </body>
73
+ </html>
74
+ `;
75
+
76
+ await writeFile(new URL("index.html", outDir), html, "utf8");
77
+ console.log(`Vercel static placeholder generated for Lattice AI ${pkg.version}`);
@@ -0,0 +1,75 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const root = process.cwd();
5
+ const readme = path.join(root, "README.md");
6
+ const checkedMarkdown = new Set();
7
+ const failures = [];
8
+
9
+ function stripAnchor(target) {
10
+ const hash = target.indexOf("#");
11
+ return hash >= 0 ? target.slice(0, hash) : target;
12
+ }
13
+
14
+ function isExternal(target) {
15
+ return /^(https?:|mailto:|tel:)/i.test(target);
16
+ }
17
+
18
+ function decodeTarget(target) {
19
+ return decodeURIComponent(target.replace(/^<|>$/g, ""));
20
+ }
21
+
22
+ function localPath(fromFile, target) {
23
+ const cleaned = stripAnchor(target).trim();
24
+ if (!cleaned || isExternal(cleaned)) return null;
25
+ return path.resolve(path.dirname(fromFile), decodeTarget(cleaned));
26
+ }
27
+
28
+ function links(markdown) {
29
+ const out = [];
30
+ const linkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
31
+ let match;
32
+ while ((match = linkPattern.exec(markdown))) {
33
+ out.push(match[1].trim());
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function checkFileLink(fromFile, target) {
39
+ const resolved = localPath(fromFile, target);
40
+ if (!resolved) return;
41
+ if (!resolved.startsWith(root)) {
42
+ failures.push(`${path.relative(root, fromFile)} links outside repo: ${target}`);
43
+ return;
44
+ }
45
+ if (!existsSync(resolved)) {
46
+ failures.push(`${path.relative(root, fromFile)} has missing link: ${target}`);
47
+ }
48
+ }
49
+
50
+ function checkMarkdownFile(file) {
51
+ if (checkedMarkdown.has(file)) return;
52
+ checkedMarkdown.add(file);
53
+ const markdown = readFileSync(file, "utf8");
54
+ for (const target of links(markdown)) {
55
+ checkFileLink(file, target);
56
+ }
57
+ }
58
+
59
+ checkMarkdownFile(readme);
60
+
61
+ for (const target of links(readFileSync(readme, "utf8"))) {
62
+ const resolved = localPath(readme, target);
63
+ if (!resolved || !existsSync(resolved)) continue;
64
+ if (statSync(resolved).isFile() && resolved.endsWith(".md")) {
65
+ checkMarkdownFile(resolved);
66
+ }
67
+ }
68
+
69
+ if (failures.length) {
70
+ console.error("Markdown link check failed:");
71
+ for (const failure of failures) console.error(`- ${failure}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ console.log(`Markdown link check passed for README and ${checkedMarkdown.size - 1} README-linked Markdown files.`);
@@ -1654,7 +1654,7 @@ dependencies = [
1654
1654
 
1655
1655
  [[package]]
1656
1656
  name = "lattice-ai-desktop"
1657
- version = "4.3.0"
1657
+ version = "4.3.3"
1658
1658
  dependencies = [
1659
1659
  "plist",
1660
1660
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "4.3.0"
3
+ version = "4.3.3"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,5 +1,7 @@
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
  };
@@ -10,6 +12,7 @@ use tauri::{Manager, State};
10
12
  struct BackendState {
11
13
  origin: String,
12
14
  command: String,
15
+ cwd: Option<String>,
13
16
  child: Mutex<Option<Child>>,
14
17
  last_error: Mutex<Option<String>>,
15
18
  }
@@ -18,11 +21,19 @@ struct BackendState {
18
21
  struct BackendStatus {
19
22
  origin: String,
20
23
  command: String,
24
+ cwd: Option<String>,
21
25
  running: bool,
22
26
  pid: Option<u32>,
23
27
  last_error: Option<String>,
24
28
  }
25
29
 
30
+ struct BackendLaunch {
31
+ command: String,
32
+ program: String,
33
+ args: Vec<String>,
34
+ cwd: Option<PathBuf>,
35
+ }
36
+
26
37
  #[tauri::command]
27
38
  fn backend_origin(state: State<'_, BackendState>) -> String {
28
39
  state.origin.clone()
@@ -36,7 +47,8 @@ fn backend_status(state: State<'_, BackendState>) -> BackendStatus {
36
47
  #[tauri::command]
37
48
  fn restart_backend(state: State<'_, BackendState>) -> BackendStatus {
38
49
  kill_backend(&state);
39
- match spawn_backend(&state.origin, &state.command) {
50
+ let launch = backend_launch(&state.origin);
51
+ match spawn_backend(&state.origin, &launch) {
40
52
  Ok(child) => {
41
53
  if let Ok(mut slot) = state.child.lock() {
42
54
  *slot = child;
@@ -55,15 +67,174 @@ fn shutdown_backend(state: State<'_, BackendState>) -> BackendStatus {
55
67
  }
56
68
 
57
69
  fn split_command(command: &str) -> Vec<String> {
58
- command
59
- .split_whitespace()
60
- .map(|part| part.to_string())
61
- .collect()
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
90
+ }
91
+
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)
62
120
  }
63
121
 
64
- fn backend_command() -> String {
65
- env::var("LATTICEAI_DESKTOP_BACKEND_CMD")
66
- .unwrap_or_else(|_| "python3 ltcai_cli.py --host 127.0.0.1 --port 8765".to_string())
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
+ }
67
238
  }
68
239
 
69
240
  fn set_error(state: &BackendState, err: Option<String>) {
@@ -72,30 +243,64 @@ fn set_error(state: &BackendState, err: Option<String>) {
72
243
  }
73
244
  }
74
245
 
75
- fn spawn_backend(origin: &str, command: &str) -> Result<Option<Child>, String> {
246
+ fn spawn_backend(origin: &str, launch: &BackendLaunch) -> Result<Option<Child>, String> {
76
247
  if env::var("LATTICEAI_DESKTOP_NO_BACKEND").is_ok() {
77
248
  return Ok(None);
78
249
  }
79
- let parts = split_command(&command);
80
- if parts.is_empty() {
81
- return Err("Desktop backend command is empty.".to_string());
250
+ if launch.program.is_empty() {
251
+ return Err("Desktop backend unavailable: LTCAI executable or importable ltcai_cli module not found.".to_string());
82
252
  }
83
- let mut cmd = Command::new(&parts[0]);
84
- cmd.args(&parts[1..])
253
+
254
+ let mut cmd = Command::new(&launch.program);
255
+ cmd.args(&launch.args)
85
256
  .env("LATTICEAI_HOST", "127.0.0.1")
86
257
  .env("LATTICEAI_PORT", origin.rsplit(':').next().unwrap_or("8765"))
87
258
  .env("LATTICEAI_ENABLE_TELEGRAM", "false")
88
259
  .env("LATTICEAI_AUTOLOAD_MODELS", "false")
260
+ .env("LATTICEAI_ALLOW_MODEL_DOWNLOADS", "false")
89
261
  .env("LATTICEAI_CORS_ALLOW_NETWORK", "false")
262
+ .env("LATTICEAI_ENABLE_EXTERNAL_CONNECTORS", "false")
90
263
  .env("LATTICEAI_TUNNEL", "false")
91
- .stdout(Stdio::null())
92
- .stderr(Stdio::null());
93
- if let Ok(cwd) = env::var("LATTICEAI_DESKTOP_BACKEND_CWD") {
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 {
94
282
  cmd.current_dir(cwd);
95
283
  }
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
+
96
301
  cmd.spawn()
97
302
  .map(Some)
98
- .map_err(|err| format!("Failed to start desktop backend '{}': {}", parts[0], err))
303
+ .map_err(|err| format!("Failed to start desktop backend '{}': {}", launch.command, err))
99
304
  }
100
305
 
101
306
  fn kill_backend(state: &BackendState) {
@@ -125,25 +330,40 @@ fn status_from_state(state: &BackendState) -> BackendStatus {
125
330
  }
126
331
  }
127
332
  }
128
- let last_error = state
129
- .last_error
130
- .lock()
131
- .ok()
132
- .and_then(|guard| guard.clone());
333
+ let last_error = state.last_error.lock().ok().and_then(|guard| guard.clone());
133
334
  BackendStatus {
134
335
  origin: state.origin.clone(),
135
336
  command: state.command.clone(),
337
+ cwd: state.cwd.clone(),
136
338
  running,
137
339
  pid,
138
340
  last_error,
139
341
  }
140
342
  }
141
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
+ }
358
+ }
359
+
142
360
  fn main() {
143
361
  let origin = env::var("LATTICEAI_DESKTOP_BACKEND_ORIGIN")
144
362
  .unwrap_or_else(|_| "http://127.0.0.1:8765".to_string());
145
- let command = backend_command();
146
- let (child, last_error) = match spawn_backend(&origin, &command) {
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) {
147
367
  Ok(child) => (child, None),
148
368
  Err(err) => (None, Some(err)),
149
369
  };
@@ -151,6 +371,7 @@ fn main() {
151
371
  .manage(BackendState {
152
372
  origin,
153
373
  command,
374
+ cwd,
154
375
  child: Mutex::new(child),
155
376
  last_error: Mutex::new(last_error),
156
377
  })
@@ -163,6 +384,17 @@ fn main() {
163
384
  .setup(|app| {
164
385
  if let Some(window) = app.get_webview_window("main") {
165
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
+ });
166
398
  }
167
399
  Ok(())
168
400
  })
@@ -173,6 +405,16 @@ fn main() {
173
405
  }
174
406
  }
175
407
  })
176
- .run(tauri::generate_context!())
177
- .expect("failed to run Lattice AI desktop shell");
408
+ .build(tauri::generate_context!())
409
+ .expect("failed to build Lattice AI desktop shell")
410
+ .run(|app_handle, event| {
411
+ if matches!(
412
+ event,
413
+ tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit
414
+ ) {
415
+ if let Some(state) = app_handle.try_state::<BackendState>() {
416
+ kill_backend(&state);
417
+ }
418
+ }
419
+ });
178
420
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "4.3.0",
4
+ "version": "4.3.3",
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.3.0",
2
+ "version": "4.3.3",
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-RiJTJliG.js",
10
- "assets/index-yZswHE3d.css": "/static/app/assets/index-yZswHE3d.css"
9
+ "index.html": "/static/app/assets/index-pdzil9ac.js",
10
+ "assets/index-CHHal8Zl.css": "/static/app/assets/index-CHHal8Zl.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-RiJTJliG.js",
20
+ "file": "assets/index-pdzil9ac.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-yZswHE3d.css"
28
+ "assets/index-CHHal8Zl.css"
29
29
  ]
30
30
  }
31
31
  }