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.
- package/README.md +186 -276
- package/bin/ltcai.js +6 -2
- package/docs/CHANGELOG.md +124 -3
- package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
- package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
- package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
- package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
- package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
- package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
- package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
- package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
- package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -25
- package/frontend/openapi.json +11 -1
- package/frontend/src/App.tsx +15 -1
- package/frontend/src/api/client.ts +19 -1
- package/frontend/src/api/openapi.ts +10 -0
- package/frontend/src/components/primitives.tsx +92 -10
- package/frontend/src/pages/Act.tsx +72 -9
- package/frontend/src/pages/Ask.tsx +2 -2
- package/frontend/src/pages/Brain.tsx +607 -65
- package/frontend/src/pages/Capture.tsx +11 -7
- package/frontend/src/pages/Library.tsx +12 -6
- package/frontend/src/pages/System.tsx +186 -23
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +3 -3
- package/lattice_brain/storage/sqlite.py +15 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +3 -1
- package/latticeai/api/models.py +66 -18
- 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 +3 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/product_hardening.py +2 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/agent_runtime.py +52 -12
- package/latticeai/services/model_runtime.py +83 -2
- package/ltcai_cli.py +14 -3
- package/package.json +5 -7
- package/requirements.txt +17 -0
- package/scripts/build_vercel_static.mjs +77 -0
- package/scripts/check_markdown_links.mjs +75 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/main.rs +269 -27
- package/src-tauri/tauri.conf.json +20 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-CHHal8Zl.css +2 -0
- package/static/app/assets/index-pdzil9ac.js +333 -0
- package/static/app/assets/index-pdzil9ac.js.map +1 -0
- package/static/app/index.html +2 -2
- package/latticeai/api/deps.py +0 -15
- package/scripts/capture/README.md +0 -28
- package/scripts/capture/capture_enterprise.js +0 -8
- package/scripts/capture/capture_graph.js +0 -8
- package/scripts/capture/capture_onboarding.js +0 -8
- package/scripts/capture/capture_page.js +0 -43
- package/scripts/capture/capture_release_media.js +0 -125
- package/scripts/capture/capture_skills.js +0 -8
- package/scripts/capture/capture_v340.js +0 -88
- package/scripts/capture/capture_workspace.js +0 -8
- package/scripts/generate_diagrams.py +0 -512
- package/scripts/release-0.3.1.sh +0 -105
- package/scripts/take_screenshots.js +0 -69
- package/static/app/assets/index-RiJTJliG.js +0 -333
- package/static/app/assets/index-RiJTJliG.js.map +0 -1
- package/static/app/assets/index-yZswHE3d.css +0 -2
- 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.
|
|
4
|
-
"description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents,
|
|
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
|
-
"
|
|
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",
|
package/requirements.txt
ADDED
|
@@ -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.`);
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
package/src-tauri/src/main.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
65
|
-
env::
|
|
66
|
-
|
|
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,
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
cmd
|
|
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
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
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 '{}': {}",
|
|
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
|
|
146
|
-
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) {
|
|
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
|
-
.
|
|
177
|
-
.expect("failed to
|
|
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.
|
|
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.
|
|
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-
|
|
10
|
-
"assets/index-
|
|
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-
|
|
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-
|
|
28
|
+
"assets/index-CHHal8Zl.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|