nexo-brain 5.3.28 → 5.3.30
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/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/nexo-brain.js +24 -7
- package/package.json +1 -1
- package/src/auto_update.py +37 -16
- package/src/cli.py +23 -0
- package/src/desktop_bridge.py +459 -0
- package/src/plugin_loader.py +5 -0
- package/src/plugins/update.py +5 -4
- package/src/scripts/nexo-cron-wrapper.sh +78 -22
- package/src/scripts/nexo-update.sh +14 -288
- package/src/server.py +140 -99
- package/src/tree_hygiene.py +56 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.30",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `5.3.
|
|
21
|
+
Version `5.3.30` is the current packaged-runtime line: four read-only CLI commands (`nexo schema`, `nexo identity`, `nexo onboard`, `nexo scan-profile`) let external UIs like NEXO Desktop auto-adapt to the editable schema, canonical identity, onboarding wizard, and profile heuristics without hardcoding fields.
|
|
22
|
+
|
|
23
|
+
Previously in `5.3.29`: duplicate `* 2` artifacts now fail hygiene gates instead of hiding in the tree, packaged/runtime update paths converge on one canonical core, startup preflight runs synchronously, corrupt DB state no longer respawns an empty brain by default, and cron runs spool locally when SQLite is unavailable.
|
|
22
24
|
|
|
23
25
|
Start here:
|
|
24
26
|
- [5-minute quickstart](docs/quickstart-5-minutes.md)
|
|
@@ -488,7 +490,9 @@ NEXO Brain doesn't just respond — it runs 13 core recovery-aware background jo
|
|
|
488
490
|
| **followup-hygiene** | Weekly (Sun) | Normalizes statuses, flags stale followups, cleans orphans |
|
|
489
491
|
| **learning-housekeep** | 03:15 daily | Dedup learnings, adjust weights by usage, process overdue reviews, reconcile decision outcomes |
|
|
490
492
|
| **immune** | Every 30 min | Quarantine processing, memory promotion/rejection, synaptic pruning |
|
|
493
|
+
| **impact-scorer** | 05:45 daily | Scores active followups so queues can prioritize by expected impact |
|
|
491
494
|
| **synthesis** | 06:00 daily | Memory synthesis — discovers cross-memory patterns |
|
|
495
|
+
| **outcome-checker** | 08:00 daily | Verifies tracked outcomes and marks them met, pending, or missed |
|
|
492
496
|
| **watchdog** | Every 30 min | Monitors services, LaunchAgents, and infrastructure health |
|
|
493
497
|
| **auto-close-sessions** | Every 5 min | Cleans stale sessions |
|
|
494
498
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -88,6 +88,20 @@ function log(msg) {
|
|
|
88
88
|
console.log(` ${msg}`);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function duplicateArtifactCanonicalName(name) {
|
|
92
|
+
const ext = path.extname(name);
|
|
93
|
+
const stem = ext ? name.slice(0, -ext.length) : name;
|
|
94
|
+
const match = stem.match(/^(.*) ([2-9]\d*)$/);
|
|
95
|
+
if (!match) return null;
|
|
96
|
+
return `${match[1]}${ext}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isDuplicateArtifactName(name, dirPath = "") {
|
|
100
|
+
const canonical = duplicateArtifactCanonicalName(name);
|
|
101
|
+
if (!canonical || !dirPath) return false;
|
|
102
|
+
return fs.existsSync(path.join(dirPath, canonical));
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
function syncWatchdogHashRegistry(nexoHome) {
|
|
92
106
|
try {
|
|
93
107
|
const watchdogPath = path.join(nexoHome, "scripts", "nexo-watchdog.sh");
|
|
@@ -122,7 +136,7 @@ function writeRuntimeCoreArtifactsManifest(nexoHome, srcDir) {
|
|
|
122
136
|
return fs.readdirSync(dirPath)
|
|
123
137
|
.filter((name) => {
|
|
124
138
|
const full = path.join(dirPath, name);
|
|
125
|
-
return fs.existsSync(full) && fs.statSync(full).isFile();
|
|
139
|
+
return fs.existsSync(full) && fs.statSync(full).isFile() && !isDuplicateArtifactName(name, dirPath);
|
|
126
140
|
})
|
|
127
141
|
.sort();
|
|
128
142
|
};
|
|
@@ -192,6 +206,7 @@ function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
|
|
|
192
206
|
const discoveredRootModules = fs.existsSync(srcDir)
|
|
193
207
|
? fs.readdirSync(srcDir)
|
|
194
208
|
.filter((name) => {
|
|
209
|
+
if (isDuplicateArtifactName(name, srcDir)) return false;
|
|
195
210
|
const stat = fs.statSync(path.join(srcDir, name));
|
|
196
211
|
if (!stat.isFile()) return false;
|
|
197
212
|
// Include Python modules and any flat JSON config the Python runtime
|
|
@@ -1551,7 +1566,7 @@ async function main() {
|
|
|
1551
1566
|
const copyDirRec = (src, dest) => {
|
|
1552
1567
|
fs.mkdirSync(dest, { recursive: true });
|
|
1553
1568
|
fs.readdirSync(src).forEach(item => {
|
|
1554
|
-
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db")) return;
|
|
1569
|
+
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db") || isDuplicateArtifactName(item, src)) return;
|
|
1555
1570
|
const srcPath = path.join(src, item);
|
|
1556
1571
|
const destPath = path.join(dest, item);
|
|
1557
1572
|
if (fs.statSync(srcPath).isDirectory()) {
|
|
@@ -1620,7 +1635,7 @@ async function main() {
|
|
|
1620
1635
|
const pluginsDest = path.join(NEXO_HOME, "plugins");
|
|
1621
1636
|
fs.mkdirSync(pluginsDest, { recursive: true });
|
|
1622
1637
|
if (fs.existsSync(pluginsSrc)) {
|
|
1623
|
-
fs.readdirSync(pluginsSrc).filter(f => f.endsWith(".py")).forEach((f) => {
|
|
1638
|
+
fs.readdirSync(pluginsSrc).filter(f => f.endsWith(".py") && !isDuplicateArtifactName(f, pluginsSrc)).forEach((f) => {
|
|
1624
1639
|
fs.copyFileSync(path.join(pluginsSrc, f), path.join(pluginsDest, f));
|
|
1625
1640
|
});
|
|
1626
1641
|
}
|
|
@@ -1772,7 +1787,7 @@ async function main() {
|
|
|
1772
1787
|
const copyDirRec2 = (src, dest) => {
|
|
1773
1788
|
fs.mkdirSync(dest, { recursive: true });
|
|
1774
1789
|
fs.readdirSync(src).forEach(item => {
|
|
1775
|
-
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db")) return;
|
|
1790
|
+
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db") || isDuplicateArtifactName(item, src)) return;
|
|
1776
1791
|
const srcP = path.join(src, item);
|
|
1777
1792
|
const destP = path.join(dest, item);
|
|
1778
1793
|
if (fs.statSync(srcP).isDirectory()) copyDirRec2(srcP, destP);
|
|
@@ -1806,7 +1821,7 @@ async function main() {
|
|
|
1806
1821
|
const copyDirRec3 = (src, dest) => {
|
|
1807
1822
|
fs.mkdirSync(dest, { recursive: true });
|
|
1808
1823
|
fs.readdirSync(src).forEach(item => {
|
|
1809
|
-
if (item === "__pycache__" || item.endsWith(".pyc")) return;
|
|
1824
|
+
if (item === "__pycache__" || item.endsWith(".pyc") || isDuplicateArtifactName(item, src)) return;
|
|
1810
1825
|
const srcP = path.join(src, item);
|
|
1811
1826
|
const destP = path.join(dest, item);
|
|
1812
1827
|
if (fs.statSync(srcP).isDirectory()) copyDirRec3(srcP, destP);
|
|
@@ -1831,6 +1846,7 @@ async function main() {
|
|
|
1831
1846
|
if (fs.existsSync(templatesSrc)) {
|
|
1832
1847
|
fs.mkdirSync(templatesDest, { recursive: true });
|
|
1833
1848
|
for (const f of fs.readdirSync(templatesSrc)) {
|
|
1849
|
+
if (isDuplicateArtifactName(f, templatesSrc)) continue;
|
|
1834
1850
|
const src = path.join(templatesSrc, f);
|
|
1835
1851
|
const dest = path.join(templatesDest, f);
|
|
1836
1852
|
if (fs.statSync(src).isFile()) {
|
|
@@ -1838,6 +1854,7 @@ async function main() {
|
|
|
1838
1854
|
} else if (fs.statSync(src).isDirectory()) {
|
|
1839
1855
|
fs.mkdirSync(dest, { recursive: true });
|
|
1840
1856
|
for (const sf of fs.readdirSync(src)) {
|
|
1857
|
+
if (isDuplicateArtifactName(sf, src)) continue;
|
|
1841
1858
|
const ssrc = path.join(src, sf);
|
|
1842
1859
|
if (fs.statSync(ssrc).isFile()) {
|
|
1843
1860
|
fs.copyFileSync(ssrc, path.join(dest, sf));
|
|
@@ -2354,7 +2371,7 @@ async function main() {
|
|
|
2354
2371
|
const copyDirRecursive = (src, dest) => {
|
|
2355
2372
|
fs.mkdirSync(dest, { recursive: true });
|
|
2356
2373
|
fs.readdirSync(src).forEach(item => {
|
|
2357
|
-
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db")) return;
|
|
2374
|
+
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db") || isDuplicateArtifactName(item, src)) return;
|
|
2358
2375
|
const srcPath = path.join(src, item);
|
|
2359
2376
|
const destPath = path.join(dest, item);
|
|
2360
2377
|
if (fs.statSync(srcPath).isDirectory()) {
|
|
@@ -2461,7 +2478,7 @@ async function main() {
|
|
|
2461
2478
|
// Plugins (all .py files in plugins/)
|
|
2462
2479
|
fs.mkdirSync(path.join(NEXO_HOME, "plugins"), { recursive: true });
|
|
2463
2480
|
if (fs.existsSync(pluginsSrcDir)) {
|
|
2464
|
-
fs.readdirSync(pluginsSrcDir).filter(f => f.endsWith(".py")).forEach((f) => {
|
|
2481
|
+
fs.readdirSync(pluginsSrcDir).filter(f => f.endsWith(".py") && !isDuplicateArtifactName(f, pluginsSrcDir)).forEach((f) => {
|
|
2465
2482
|
fs.copyFileSync(path.join(pluginsSrcDir, f), path.join(NEXO_HOME, "plugins", f));
|
|
2466
2483
|
});
|
|
2467
2484
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.30",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/auto_update.py
CHANGED
|
@@ -18,6 +18,7 @@ import time
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
21
|
+
from tree_hygiene import is_duplicate_artifact_name
|
|
21
22
|
|
|
22
23
|
NEXO_HOME = export_resolved_nexo_home()
|
|
23
24
|
DATA_DIR = NEXO_HOME / "data"
|
|
@@ -63,6 +64,20 @@ def _log(msg: str):
|
|
|
63
64
|
print(f"[NEXO auto-update] {msg}", file=sys.stderr)
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
def _runtime_copy_ignore(*extra_patterns: str):
|
|
68
|
+
base_ignore = shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.db", *extra_patterns)
|
|
69
|
+
|
|
70
|
+
def _ignore(dir_name: str, names: list[str]) -> set[str]:
|
|
71
|
+
ignored = set(base_ignore(dir_name, names))
|
|
72
|
+
ignored.update(
|
|
73
|
+
name for name in names
|
|
74
|
+
if is_duplicate_artifact_name(Path(dir_name) / name)
|
|
75
|
+
)
|
|
76
|
+
return ignored
|
|
77
|
+
|
|
78
|
+
return _ignore
|
|
79
|
+
|
|
80
|
+
|
|
66
81
|
def _critical_table_count(db_path: Path, table: str) -> int | None:
|
|
67
82
|
"""Return COUNT(*) for a critical table when it exists, otherwise None."""
|
|
68
83
|
import sqlite3
|
|
@@ -470,7 +485,7 @@ def _refresh_installed_manifest():
|
|
|
470
485
|
if src_crons.exists():
|
|
471
486
|
dst_crons.mkdir(parents=True, exist_ok=True)
|
|
472
487
|
for f in src_crons.iterdir():
|
|
473
|
-
if f.is_file():
|
|
488
|
+
if f.is_file() and not is_duplicate_artifact_name(f):
|
|
474
489
|
shutil.copy2(str(f), str(dst_crons / f.name))
|
|
475
490
|
_log("Refreshed installed crons manifest")
|
|
476
491
|
except Exception as e:
|
|
@@ -746,7 +761,7 @@ def _sync_hooks():
|
|
|
746
761
|
hooks_dest.mkdir(parents=True, exist_ok=True)
|
|
747
762
|
synced = 0
|
|
748
763
|
for f in hooks_src.iterdir():
|
|
749
|
-
if f.is_file() and f.suffix == ".sh":
|
|
764
|
+
if f.is_file() and f.suffix == ".sh" and not is_duplicate_artifact_name(f):
|
|
750
765
|
dest = hooks_dest / f.name
|
|
751
766
|
shutil.copy2(str(f), str(dest))
|
|
752
767
|
os.chmod(str(dest), 0o755)
|
|
@@ -1441,7 +1456,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
1441
1456
|
import shutil
|
|
1442
1457
|
scripts_dest.mkdir(parents=True, exist_ok=True)
|
|
1443
1458
|
for f in scripts_src.iterdir():
|
|
1444
|
-
if f.name.startswith('.') or f.name == '__pycache__':
|
|
1459
|
+
if f.name.startswith('.') or f.name == '__pycache__' or is_duplicate_artifact_name(f):
|
|
1445
1460
|
continue
|
|
1446
1461
|
dest = scripts_dest / f.name
|
|
1447
1462
|
if f.is_file() and not dest.exists():
|
|
@@ -1475,12 +1490,12 @@ def _auto_update_check_locked() -> dict:
|
|
|
1475
1490
|
if doctor_src.is_dir():
|
|
1476
1491
|
import shutil
|
|
1477
1492
|
if not doctor_dest.is_dir():
|
|
1478
|
-
shutil.copytree(str(doctor_src), str(doctor_dest), ignore=
|
|
1493
|
+
shutil.copytree(str(doctor_src), str(doctor_dest), ignore=_runtime_copy_ignore())
|
|
1479
1494
|
_log("Backfilled doctor package")
|
|
1480
1495
|
else:
|
|
1481
1496
|
# Update existing files
|
|
1482
1497
|
for root, dirs, files in os.walk(str(doctor_src)):
|
|
1483
|
-
dirs[:] = [d for d in dirs if d != "__pycache__"]
|
|
1498
|
+
dirs[:] = [d for d in dirs if d != "__pycache__" and not is_duplicate_artifact_name(Path(root) / d)]
|
|
1484
1499
|
rel = os.path.relpath(root, str(doctor_src))
|
|
1485
1500
|
dest_dir = doctor_dest / rel
|
|
1486
1501
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1488,6 +1503,8 @@ def _auto_update_check_locked() -> dict:
|
|
|
1488
1503
|
if f.endswith(".pyc"):
|
|
1489
1504
|
continue
|
|
1490
1505
|
src_f = Path(root) / f
|
|
1506
|
+
if is_duplicate_artifact_name(src_f):
|
|
1507
|
+
continue
|
|
1491
1508
|
dst_f = dest_dir / f
|
|
1492
1509
|
if not dst_f.exists() or src_f.stat().st_mtime > dst_f.stat().st_mtime:
|
|
1493
1510
|
shutil.copy2(str(src_f), str(dst_f))
|
|
@@ -1501,11 +1518,11 @@ def _auto_update_check_locked() -> dict:
|
|
|
1501
1518
|
if skills_src.is_dir():
|
|
1502
1519
|
import shutil
|
|
1503
1520
|
if not skills_dest.is_dir():
|
|
1504
|
-
shutil.copytree(str(skills_src), str(skills_dest), ignore=
|
|
1521
|
+
shutil.copytree(str(skills_src), str(skills_dest), ignore=_runtime_copy_ignore())
|
|
1505
1522
|
_log("Backfilled skills-core")
|
|
1506
1523
|
else:
|
|
1507
1524
|
for root, dirs, files in os.walk(str(skills_src)):
|
|
1508
|
-
dirs[:] = [d for d in dirs if d != "__pycache__"]
|
|
1525
|
+
dirs[:] = [d for d in dirs if d != "__pycache__" and not is_duplicate_artifact_name(Path(root) / d)]
|
|
1509
1526
|
rel = os.path.relpath(root, str(skills_src))
|
|
1510
1527
|
dest_dir = skills_dest / rel
|
|
1511
1528
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1513,6 +1530,8 @@ def _auto_update_check_locked() -> dict:
|
|
|
1513
1530
|
if f.endswith(".pyc"):
|
|
1514
1531
|
continue
|
|
1515
1532
|
src_f = Path(root) / f
|
|
1533
|
+
if is_duplicate_artifact_name(src_f):
|
|
1534
|
+
continue
|
|
1516
1535
|
dst_f = dest_dir / f
|
|
1517
1536
|
if not dst_f.exists() or src_f.stat().st_mtime > dst_f.stat().st_mtime:
|
|
1518
1537
|
shutil.copy2(str(src_f), str(dst_f))
|
|
@@ -1539,7 +1558,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
1539
1558
|
import shutil
|
|
1540
1559
|
if templates_src.is_dir():
|
|
1541
1560
|
for item in templates_src.iterdir():
|
|
1542
|
-
if item.name == "__pycache__":
|
|
1561
|
+
if item.name == "__pycache__" or is_duplicate_artifact_name(item):
|
|
1543
1562
|
continue
|
|
1544
1563
|
dest_item = templates_dest / item.name
|
|
1545
1564
|
if item.is_file():
|
|
@@ -1548,7 +1567,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
1548
1567
|
elif item.is_dir():
|
|
1549
1568
|
dest_item.mkdir(parents=True, exist_ok=True)
|
|
1550
1569
|
for sub in item.iterdir():
|
|
1551
|
-
if sub.is_file():
|
|
1570
|
+
if sub.is_file() and not is_duplicate_artifact_name(sub):
|
|
1552
1571
|
dest_sub = dest_item / sub.name
|
|
1553
1572
|
if not dest_sub.exists() or sub.stat().st_mtime > dest_sub.stat().st_mtime:
|
|
1554
1573
|
shutil.copy2(str(sub), str(dest_sub))
|
|
@@ -1735,6 +1754,8 @@ def _discover_runtime_root_python_modules(base_dir: Path) -> list[str]:
|
|
|
1735
1754
|
continue
|
|
1736
1755
|
if item.name.startswith(".") or item.name == "__init__.py":
|
|
1737
1756
|
continue
|
|
1757
|
+
if is_duplicate_artifact_name(item):
|
|
1758
|
+
continue
|
|
1738
1759
|
modules.append(item.name)
|
|
1739
1760
|
return modules
|
|
1740
1761
|
|
|
@@ -1857,7 +1878,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1857
1878
|
shutil.copytree(
|
|
1858
1879
|
str(pkg_src),
|
|
1859
1880
|
str(pkg_dest),
|
|
1860
|
-
ignore=
|
|
1881
|
+
ignore=_runtime_copy_ignore(),
|
|
1861
1882
|
)
|
|
1862
1883
|
copied_packages += 1
|
|
1863
1884
|
|
|
@@ -1874,7 +1895,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1874
1895
|
if plugins_src.is_dir():
|
|
1875
1896
|
plugins_dest.mkdir(parents=True, exist_ok=True)
|
|
1876
1897
|
for item in plugins_src.iterdir():
|
|
1877
|
-
if item.is_file() and item.suffix == ".py":
|
|
1898
|
+
if item.is_file() and item.suffix == ".py" and not is_duplicate_artifact_name(item):
|
|
1878
1899
|
shutil.copy2(str(item), str(plugins_dest / item.name))
|
|
1879
1900
|
|
|
1880
1901
|
_emit_progress(progress_fn, "Copying scripts...")
|
|
@@ -1883,13 +1904,13 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1883
1904
|
if scripts_src.is_dir():
|
|
1884
1905
|
scripts_dest.mkdir(parents=True, exist_ok=True)
|
|
1885
1906
|
for item in scripts_src.iterdir():
|
|
1886
|
-
if item.name == "__pycache__" or item.name.startswith("."):
|
|
1907
|
+
if item.name == "__pycache__" or item.name.startswith(".") or is_duplicate_artifact_name(item):
|
|
1887
1908
|
continue
|
|
1888
1909
|
dst = scripts_dest / item.name
|
|
1889
1910
|
if item.is_dir():
|
|
1890
1911
|
if dst.exists():
|
|
1891
1912
|
shutil.rmtree(str(dst), ignore_errors=True)
|
|
1892
|
-
shutil.copytree(str(item), str(dst), ignore=
|
|
1913
|
+
shutil.copytree(str(item), str(dst), ignore=_runtime_copy_ignore())
|
|
1893
1914
|
elif item.is_file():
|
|
1894
1915
|
existing_class = installed_script_classes.get(item.name, "")
|
|
1895
1916
|
if dst.exists() and existing_class in {"personal", "non-script"}:
|
|
@@ -1919,7 +1940,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1919
1940
|
if templates_src.is_dir():
|
|
1920
1941
|
templates_dest.mkdir(parents=True, exist_ok=True)
|
|
1921
1942
|
for item in templates_src.iterdir():
|
|
1922
|
-
if item.name == "__pycache__":
|
|
1943
|
+
if item.name == "__pycache__" or is_duplicate_artifact_name(item):
|
|
1923
1944
|
continue
|
|
1924
1945
|
if item.is_file():
|
|
1925
1946
|
shutil.copy2(str(item), str(templates_dest / item.name))
|
|
@@ -1927,7 +1948,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1927
1948
|
sub_dest = templates_dest / item.name
|
|
1928
1949
|
sub_dest.mkdir(parents=True, exist_ok=True)
|
|
1929
1950
|
for sub in item.iterdir():
|
|
1930
|
-
if sub.is_file():
|
|
1951
|
+
if sub.is_file() and not is_duplicate_artifact_name(sub):
|
|
1931
1952
|
shutil.copy2(str(sub), str(sub_dest / sub.name))
|
|
1932
1953
|
|
|
1933
1954
|
package_json = repo_dir / "package.json"
|
|
@@ -1948,7 +1969,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1948
1969
|
if skills_src.is_dir():
|
|
1949
1970
|
if skills_dest.exists():
|
|
1950
1971
|
shutil.rmtree(str(skills_dest), ignore_errors=True)
|
|
1951
|
-
shutil.copytree(str(skills_src), str(skills_dest), ignore=
|
|
1972
|
+
shutil.copytree(str(skills_src), str(skills_dest), ignore=_runtime_copy_ignore())
|
|
1952
1973
|
|
|
1953
1974
|
bin_dir = dest / "bin"
|
|
1954
1975
|
bin_dir.mkdir(parents=True, exist_ok=True)
|
package/src/cli.py
CHANGED
|
@@ -1944,6 +1944,21 @@ def main():
|
|
|
1944
1944
|
dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
|
|
1945
1945
|
dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
|
|
1946
1946
|
|
|
1947
|
+
# -- desktop bridge (read-only, for NEXO Desktop and any external UI) --
|
|
1948
|
+
schema_parser = sub.add_parser("schema", help="Editable-field schema for Preferences UI")
|
|
1949
|
+
schema_parser.add_argument("--json", action="store_true", help="JSON output (default)")
|
|
1950
|
+
|
|
1951
|
+
identity_parser = sub.add_parser("identity", help="Canonical assistant identity")
|
|
1952
|
+
identity_parser.add_argument("--json", action="store_true", help="JSON output (default)")
|
|
1953
|
+
|
|
1954
|
+
onboard_parser = sub.add_parser("onboard", help="Onboarding wizard steps")
|
|
1955
|
+
onboard_parser.add_argument("--json", action="store_true", help="JSON output (default)")
|
|
1956
|
+
|
|
1957
|
+
scan_profile_parser = sub.add_parser("scan-profile", help="Build profile.json from CLAUDE.md + calibration")
|
|
1958
|
+
scan_profile_parser.add_argument("--json", action="store_true", help="JSON output")
|
|
1959
|
+
scan_profile_parser.add_argument("--apply", action="store_true", help="Write profile.json (default is preview)")
|
|
1960
|
+
scan_profile_parser.add_argument("--force", action="store_true", help="Overwrite existing profile.json on --apply")
|
|
1961
|
+
|
|
1947
1962
|
args = parser.parse_args()
|
|
1948
1963
|
|
|
1949
1964
|
if args.help or (not args.command and not args.version):
|
|
@@ -2037,6 +2052,14 @@ def main():
|
|
|
2037
2052
|
return _uninstall(args)
|
|
2038
2053
|
elif args.command == "dashboard":
|
|
2039
2054
|
return _dashboard(args)
|
|
2055
|
+
elif args.command in ("schema", "identity", "onboard", "scan-profile"):
|
|
2056
|
+
from desktop_bridge import cmd_schema, cmd_identity, cmd_onboard, cmd_scan_profile
|
|
2057
|
+
return {
|
|
2058
|
+
"schema": cmd_schema,
|
|
2059
|
+
"identity": cmd_identity,
|
|
2060
|
+
"onboard": cmd_onboard,
|
|
2061
|
+
"scan-profile": cmd_scan_profile,
|
|
2062
|
+
}[args.command](args)
|
|
2040
2063
|
else:
|
|
2041
2064
|
_print_help()
|
|
2042
2065
|
return 0
|