nexo-brain 7.25.2 → 7.25.4
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 +3 -1
- package/bin/nexo-brain.js +25 -7
- package/package.json +1 -1
- package/src/auto_update.py +94 -8
- package/src/cli.py +66 -0
- package/src/db/_schema.py +23 -0
- package/src/doctor/providers/boot.py +106 -0
- package/src/local_context/__init__.py +10 -0
- package/src/local_context/api.py +832 -47
- package/src/local_context/db.py +45 -1
- package/src/local_context/extractors.py +17 -1
- package/src/plugins/update.py +88 -1
- package/src/server.py +26 -0
- package/tool-enforcement-map.json +31 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.25.
|
|
3
|
+
"version": "7.25.4",
|
|
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 `7.25.
|
|
21
|
+
Version `7.25.4` is the current packaged-runtime line. Patch release over v7.25.3 - Local Memory starts from safe user-content and email roots, adds configurable included/excluded file types, and cleans legacy whole-disk index state with backup or archive-rebuild safety.
|
|
22
|
+
|
|
23
|
+
Previously in `7.25.3`: patch release over v7.25.2 - Desktop-managed Brain installs require the same Python ABI as the bundled wheels and repair incompatible managed virtual environments before reuse.
|
|
22
24
|
|
|
23
25
|
Previously in `7.25.0`: minor release over v7.24.0 - Memory Fabric links transcript lookup, historical backup diary recovery, unified search and knowledge graph evidence so memories are not available only inside expiring snapshots.
|
|
24
26
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -57,6 +57,8 @@ const DEFAULT_ASSISTANT_NAME = "Nova";
|
|
|
57
57
|
const RESERVED_ASSISTANT_NAME_KEYS = new Set(["nexo", "nexobrain", "nexodesktop"]);
|
|
58
58
|
const MIN_INSTALLER_PYTHON_MAJOR = 3;
|
|
59
59
|
const MIN_INSTALLER_PYTHON_MINOR = 10;
|
|
60
|
+
const DESKTOP_BUNDLED_WHEEL_PYTHON_MAJOR = 3;
|
|
61
|
+
const DESKTOP_BUNDLED_WHEEL_PYTHON_MINOR = 12;
|
|
60
62
|
|
|
61
63
|
function normalizeAssistantNameCandidate(value) {
|
|
62
64
|
return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
@@ -278,11 +280,25 @@ function pythonVersionMeetsMinimum(versionText) {
|
|
|
278
280
|
|| (major === MIN_INSTALLER_PYTHON_MAJOR && minor >= MIN_INSTALLER_PYTHON_MINOR);
|
|
279
281
|
}
|
|
280
282
|
|
|
283
|
+
function pythonVersionMatchesDesktopBundle(versionText) {
|
|
284
|
+
const match = String(versionText || "").trim().match(/^(\d+)\.(\d+)(?:\.|$)/);
|
|
285
|
+
if (!match) return false;
|
|
286
|
+
return Number(match[1]) === DESKTOP_BUNDLED_WHEEL_PYTHON_MAJOR
|
|
287
|
+
&& Number(match[2]) === DESKTOP_BUNDLED_WHEEL_PYTHON_MINOR;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function pythonVersionUsableForInstaller(versionText) {
|
|
291
|
+
if (!pythonVersionMeetsMinimum(versionText)) return false;
|
|
292
|
+
if (isDesktopManagedInstall()) return pythonVersionMatchesDesktopBundle(versionText);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
281
296
|
function resolveInstallerPython() {
|
|
282
297
|
const candidates = [
|
|
283
298
|
process.env.NEXO_BOOTSTRAP_PYTHON,
|
|
284
299
|
process.env.NEXO_RUNTIME_PYTHON,
|
|
285
300
|
process.env.NEXO_PYTHON,
|
|
301
|
+
run("which python3.12"),
|
|
286
302
|
run("which python3"),
|
|
287
303
|
run("which python"),
|
|
288
304
|
].filter(Boolean);
|
|
@@ -292,7 +308,7 @@ function resolveInstallerPython() {
|
|
|
292
308
|
if (!clean || seen.has(clean)) continue;
|
|
293
309
|
seen.add(clean);
|
|
294
310
|
const version = pythonVersion(clean);
|
|
295
|
-
if (version &&
|
|
311
|
+
if (version && pythonVersionUsableForInstaller(version)) return clean;
|
|
296
312
|
}
|
|
297
313
|
return "";
|
|
298
314
|
}
|
|
@@ -366,9 +382,11 @@ function uniqueBackupPath(targetPath, suffix) {
|
|
|
366
382
|
function ensureManagedVenvCompatible(venvPath, venvPython) {
|
|
367
383
|
if (!fs.existsSync(venvPython)) return;
|
|
368
384
|
const version = pythonVersion(venvPython);
|
|
369
|
-
if (version &&
|
|
385
|
+
if (version && pythonVersionUsableForInstaller(version)) return;
|
|
370
386
|
|
|
371
|
-
const reason = version
|
|
387
|
+
const reason = version
|
|
388
|
+
? `Python ${version}${isDesktopManagedInstall() ? " (Desktop bundle requires Python 3.12)" : ""}`
|
|
389
|
+
: "an unreadable Python executable";
|
|
372
390
|
const backupPath = uniqueBackupPath(venvPath, "unsupported-python");
|
|
373
391
|
log(` Existing Python virtual environment uses ${reason}; moving it aside to recreate.`);
|
|
374
392
|
try {
|
|
@@ -3300,14 +3318,14 @@ async function runSetup() {
|
|
|
3300
3318
|
}
|
|
3301
3319
|
if (!python) {
|
|
3302
3320
|
log("Python 3 not found and couldn't install automatically.");
|
|
3303
|
-
log(platform === "darwin" ? "Install it: brew install
|
|
3321
|
+
log(platform === "darwin" ? "Install it: brew install python@3.12" : "Install it: sudo apt install python3");
|
|
3304
3322
|
process.exit(1);
|
|
3305
3323
|
}
|
|
3306
3324
|
}
|
|
3307
3325
|
const pyVersion = pythonVersion(python);
|
|
3308
|
-
if (!pyVersion || !
|
|
3326
|
+
if (!pyVersion || !pythonVersionUsableForInstaller(pyVersion)) {
|
|
3309
3327
|
log(pyVersion
|
|
3310
|
-
? `Python at ${python} is ${pyVersion}; NEXO Brain requires Python >=${MIN_INSTALLER_PYTHON_MAJOR}.${MIN_INSTALLER_PYTHON_MINOR}.`
|
|
3328
|
+
? `Python at ${python} is ${pyVersion}; NEXO Brain requires ${isDesktopManagedInstall() ? "Python 3.12 for Desktop bundled wheels" : `Python >=${MIN_INSTALLER_PYTHON_MAJOR}.${MIN_INSTALLER_PYTHON_MINOR}`}.`
|
|
3311
3329
|
: `Python at ${python || "(not found)"} is not executable.`);
|
|
3312
3330
|
process.exit(1);
|
|
3313
3331
|
}
|
|
@@ -3618,7 +3636,7 @@ async function runSetup() {
|
|
|
3618
3636
|
}
|
|
3619
3637
|
if (fs.existsSync(venvPython)) {
|
|
3620
3638
|
const venvVersion = pythonVersion(venvPython);
|
|
3621
|
-
if (!venvVersion || !
|
|
3639
|
+
if (!venvVersion || !pythonVersionUsableForInstaller(venvVersion)) {
|
|
3622
3640
|
log(`Python virtual environment is unsupported after creation (${venvVersion || "unknown version"}).`);
|
|
3623
3641
|
process.exit(1);
|
|
3624
3642
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.25.
|
|
3
|
+
"version": "7.25.4",
|
|
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
|
@@ -21,7 +21,7 @@ import time
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
|
-
from product_mode import enforce_desktop_product_contract
|
|
24
|
+
from product_mode import desktop_product_requested, enforce_desktop_product_contract
|
|
25
25
|
except ModuleNotFoundError as exc:
|
|
26
26
|
if getattr(exc, "name", "") != "product_mode":
|
|
27
27
|
raise
|
|
@@ -30,7 +30,7 @@ except ModuleNotFoundError as exc:
|
|
|
30
30
|
core_path = str(_core_runtime)
|
|
31
31
|
if core_path not in sys.path:
|
|
32
32
|
sys.path.insert(0, core_path)
|
|
33
|
-
from product_mode import enforce_desktop_product_contract
|
|
33
|
+
from product_mode import desktop_product_requested, enforce_desktop_product_contract
|
|
34
34
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
35
35
|
|
|
36
36
|
try:
|
|
@@ -939,14 +939,94 @@ def _venv_pip_path(runtime_root: Path = NEXO_HOME) -> Path:
|
|
|
939
939
|
return runtime_root / ".venv" / "bin" / "pip"
|
|
940
940
|
|
|
941
941
|
|
|
942
|
+
def _python_version_tuple(python_bin: Path | str) -> tuple[int, int, int] | None:
|
|
943
|
+
try:
|
|
944
|
+
result = subprocess.run(
|
|
945
|
+
[str(python_bin), "-c", "import sys; print('.'.join(map(str, sys.version_info[:3])))"],
|
|
946
|
+
capture_output=True,
|
|
947
|
+
text=True,
|
|
948
|
+
timeout=15,
|
|
949
|
+
)
|
|
950
|
+
except Exception:
|
|
951
|
+
return None
|
|
952
|
+
if result.returncode != 0:
|
|
953
|
+
return None
|
|
954
|
+
match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout or result.stderr or "")
|
|
955
|
+
if not match:
|
|
956
|
+
return None
|
|
957
|
+
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _managed_venv_python_supported(python_bin: Path | str) -> bool:
|
|
961
|
+
version = _python_version_tuple(python_bin)
|
|
962
|
+
if not version:
|
|
963
|
+
return False
|
|
964
|
+
if desktop_product_requested():
|
|
965
|
+
return version[:2] == (3, 12)
|
|
966
|
+
return version >= (3, 10, 0)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _resolve_managed_venv_base_python() -> str:
|
|
970
|
+
candidates = [
|
|
971
|
+
os.environ.get("NEXO_BOOTSTRAP_PYTHON", ""),
|
|
972
|
+
os.environ.get("NEXO_RUNTIME_PYTHON", ""),
|
|
973
|
+
os.environ.get("NEXO_PYTHON", ""),
|
|
974
|
+
]
|
|
975
|
+
if desktop_product_requested():
|
|
976
|
+
candidates.extend([
|
|
977
|
+
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
|
|
978
|
+
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
|
|
979
|
+
"/opt/homebrew/bin/python3.12",
|
|
980
|
+
"/usr/local/bin/python3.12",
|
|
981
|
+
shutil.which("python3.12") or "",
|
|
982
|
+
])
|
|
983
|
+
candidates.append(sys.executable)
|
|
984
|
+
candidates.append(shutil.which("python3") or "")
|
|
985
|
+
for candidate in candidates:
|
|
986
|
+
clean = str(candidate or "").strip()
|
|
987
|
+
if clean and Path(clean).exists() and _managed_venv_python_supported(clean):
|
|
988
|
+
return clean
|
|
989
|
+
return sys.executable
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _archive_incompatible_runtime_venv(runtime_root: Path = NEXO_HOME, reason: str = "incompatible-python") -> Path | None:
|
|
993
|
+
venv_dir = runtime_root / ".venv"
|
|
994
|
+
if not venv_dir.exists():
|
|
995
|
+
return None
|
|
996
|
+
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
997
|
+
backup_root = runtime_root / "runtime" / "backups"
|
|
998
|
+
try:
|
|
999
|
+
backup_root.mkdir(parents=True, exist_ok=True)
|
|
1000
|
+
target = backup_root / f"venv-{reason}-{stamp}"
|
|
1001
|
+
counter = 2
|
|
1002
|
+
while target.exists():
|
|
1003
|
+
target = backup_root / f"venv-{reason}-{stamp}-{counter}"
|
|
1004
|
+
counter += 1
|
|
1005
|
+
shutil.move(str(venv_dir), str(target))
|
|
1006
|
+
_log(f"Archived incompatible managed venv at {target}")
|
|
1007
|
+
return target
|
|
1008
|
+
except Exception as exc:
|
|
1009
|
+
_log(f"venv archive failed: {exc}")
|
|
1010
|
+
return None
|
|
1011
|
+
|
|
1012
|
+
|
|
942
1013
|
def _ensure_runtime_venv(runtime_root: Path = NEXO_HOME) -> Path | None:
|
|
943
1014
|
venv_python = _venv_python_path(runtime_root)
|
|
944
1015
|
if venv_python.exists():
|
|
945
|
-
|
|
1016
|
+
if _managed_venv_python_supported(venv_python):
|
|
1017
|
+
return venv_python
|
|
1018
|
+
version = _python_version_tuple(venv_python)
|
|
1019
|
+
reason = f"python-{'.'.join(map(str, version[:2]))}" if version else "unreadable-python"
|
|
1020
|
+
if _archive_incompatible_runtime_venv(runtime_root, reason=reason) is None:
|
|
1021
|
+
return None
|
|
1022
|
+
base_python = _resolve_managed_venv_base_python()
|
|
1023
|
+
if not _managed_venv_python_supported(base_python):
|
|
1024
|
+
_log(f"no supported base Python found for managed venv: {base_python}")
|
|
1025
|
+
return None
|
|
946
1026
|
try:
|
|
947
1027
|
runtime_root.mkdir(parents=True, exist_ok=True)
|
|
948
1028
|
result = subprocess.run(
|
|
949
|
-
[
|
|
1029
|
+
[base_python, "-m", "venv", str(runtime_root / ".venv")],
|
|
950
1030
|
capture_output=True,
|
|
951
1031
|
text=True,
|
|
952
1032
|
timeout=120,
|
|
@@ -965,7 +1045,7 @@ def _reinstall_pip_deps() -> bool:
|
|
|
965
1045
|
req_file = SRC_DIR / "requirements.txt"
|
|
966
1046
|
if not req_file.exists():
|
|
967
1047
|
return True
|
|
968
|
-
_ensure_runtime_venv(NEXO_HOME)
|
|
1048
|
+
venv_python = _ensure_runtime_venv(NEXO_HOME)
|
|
969
1049
|
venv_pip = _venv_pip_path(NEXO_HOME)
|
|
970
1050
|
if not venv_pip.exists() and sys.platform != "win32":
|
|
971
1051
|
alt_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
|
|
@@ -977,11 +1057,14 @@ def _reinstall_pip_deps() -> bool:
|
|
|
977
1057
|
[str(venv_pip), "install", "--quiet", "-r", str(req_file)],
|
|
978
1058
|
capture_output=True, text=True, timeout=120,
|
|
979
1059
|
)
|
|
980
|
-
|
|
1060
|
+
elif not desktop_product_requested():
|
|
981
1061
|
result = subprocess.run(
|
|
982
1062
|
[sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
|
|
983
1063
|
capture_output=True, text=True, timeout=120,
|
|
984
1064
|
)
|
|
1065
|
+
else:
|
|
1066
|
+
_log(f"managed venv unavailable for Desktop dependency repair: {venv_python}")
|
|
1067
|
+
return False
|
|
985
1068
|
if result.returncode != 0:
|
|
986
1069
|
_log(f"pip install failed (exit {result.returncode}): {result.stderr or result.stdout}")
|
|
987
1070
|
return False
|
|
@@ -4665,7 +4748,7 @@ def _reinstall_runtime_pip_deps(runtime_root: Path = NEXO_HOME) -> bool:
|
|
|
4665
4748
|
req_file = runtime_root / "requirements.txt"
|
|
4666
4749
|
if not req_file.exists():
|
|
4667
4750
|
return True
|
|
4668
|
-
_ensure_runtime_venv(runtime_root)
|
|
4751
|
+
venv_python = _ensure_runtime_venv(runtime_root)
|
|
4669
4752
|
venv_pip = _venv_pip_path(runtime_root)
|
|
4670
4753
|
if not venv_pip.exists() and sys.platform != "win32":
|
|
4671
4754
|
alt_pip = runtime_root / ".venv" / "bin" / "pip3"
|
|
@@ -4679,13 +4762,16 @@ def _reinstall_runtime_pip_deps(runtime_root: Path = NEXO_HOME) -> bool:
|
|
|
4679
4762
|
text=True,
|
|
4680
4763
|
timeout=120,
|
|
4681
4764
|
)
|
|
4682
|
-
|
|
4765
|
+
elif not desktop_product_requested():
|
|
4683
4766
|
result = subprocess.run(
|
|
4684
4767
|
[sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
|
|
4685
4768
|
capture_output=True,
|
|
4686
4769
|
text=True,
|
|
4687
4770
|
timeout=120,
|
|
4688
4771
|
)
|
|
4772
|
+
else:
|
|
4773
|
+
_log(f"managed venv unavailable for Desktop runtime dependency repair: {venv_python}")
|
|
4774
|
+
return False
|
|
4689
4775
|
return result.returncode == 0
|
|
4690
4776
|
except Exception:
|
|
4691
4777
|
return False
|
package/src/cli.py
CHANGED
|
@@ -1853,6 +1853,42 @@ def _local_context_exclusions(args) -> int:
|
|
|
1853
1853
|
return _local_context_emit({"ok": False, "error": f"unsupported exclusions command: {command}"}, args)
|
|
1854
1854
|
|
|
1855
1855
|
|
|
1856
|
+
def _local_context_filetypes(args) -> int:
|
|
1857
|
+
import local_context
|
|
1858
|
+
command = str(getattr(args, "local_context_filetypes_command", "") or "")
|
|
1859
|
+
if command == "list":
|
|
1860
|
+
return _local_context_emit(local_context.list_file_type_rules(readonly=False), args)
|
|
1861
|
+
if command == "include":
|
|
1862
|
+
return _local_context_emit(
|
|
1863
|
+
local_context.set_file_type_rule(
|
|
1864
|
+
getattr(args, "extension", ""),
|
|
1865
|
+
action=getattr(args, "action", "extract") or "extract",
|
|
1866
|
+
reason=getattr(args, "reason", "user") or "user",
|
|
1867
|
+
),
|
|
1868
|
+
args,
|
|
1869
|
+
)
|
|
1870
|
+
if command == "exclude":
|
|
1871
|
+
return _local_context_emit(
|
|
1872
|
+
local_context.set_file_type_rule(
|
|
1873
|
+
getattr(args, "extension", ""),
|
|
1874
|
+
action="ignore",
|
|
1875
|
+
reason=getattr(args, "reason", "user") or "user",
|
|
1876
|
+
),
|
|
1877
|
+
args,
|
|
1878
|
+
)
|
|
1879
|
+
if command == "remove":
|
|
1880
|
+
return _local_context_emit(local_context.remove_file_type_rule(getattr(args, "extension", "")), args)
|
|
1881
|
+
if command == "reset":
|
|
1882
|
+
return _local_context_emit(local_context.reset_file_type_rules(), args)
|
|
1883
|
+
return _local_context_emit({"ok": False, "error": f"unsupported filetypes command: {command}"}, args)
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def _local_context_migrate_roots_v2(args) -> int:
|
|
1887
|
+
import local_context
|
|
1888
|
+
dry_run = not bool(getattr(args, "apply", False))
|
|
1889
|
+
return _local_context_emit(local_context.migrate_roots_seed_v2(dry_run=dry_run), args)
|
|
1890
|
+
|
|
1891
|
+
|
|
1856
1892
|
def _local_context_query(args) -> int:
|
|
1857
1893
|
import local_context
|
|
1858
1894
|
return _local_context_emit(
|
|
@@ -3759,6 +3795,29 @@ def main():
|
|
|
3759
3795
|
local_context_exclusions_remove_p.add_argument("path", help="Folder path")
|
|
3760
3796
|
local_context_exclusions_remove_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3761
3797
|
|
|
3798
|
+
local_context_filetypes_p = local_context_sub.add_parser("filetypes", help="Manage included and excluded file extensions")
|
|
3799
|
+
local_context_filetypes_sub = local_context_filetypes_p.add_subparsers(dest="local_context_filetypes_command")
|
|
3800
|
+
local_context_filetypes_list_p = local_context_filetypes_sub.add_parser("list", help="List file type rules")
|
|
3801
|
+
local_context_filetypes_list_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3802
|
+
local_context_filetypes_include_p = local_context_filetypes_sub.add_parser("include", help="Include a file extension")
|
|
3803
|
+
local_context_filetypes_include_p.add_argument("extension", help="Extension, for example .pdf or asd")
|
|
3804
|
+
local_context_filetypes_include_p.add_argument("--action", choices=["extract", "metadata"], default="extract", help="How to index this extension")
|
|
3805
|
+
local_context_filetypes_include_p.add_argument("--reason", default="user", help="Reason label")
|
|
3806
|
+
local_context_filetypes_include_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3807
|
+
local_context_filetypes_exclude_p = local_context_filetypes_sub.add_parser("exclude", help="Exclude a file extension")
|
|
3808
|
+
local_context_filetypes_exclude_p.add_argument("extension", help="Extension, for example .jpg or tmp")
|
|
3809
|
+
local_context_filetypes_exclude_p.add_argument("--reason", default="user", help="Reason label")
|
|
3810
|
+
local_context_filetypes_exclude_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3811
|
+
local_context_filetypes_remove_p = local_context_filetypes_sub.add_parser("remove", help="Remove a user extension override")
|
|
3812
|
+
local_context_filetypes_remove_p.add_argument("extension", help="Extension to reset")
|
|
3813
|
+
local_context_filetypes_remove_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3814
|
+
local_context_filetypes_reset_p = local_context_filetypes_sub.add_parser("reset", help="Remove all user extension overrides")
|
|
3815
|
+
local_context_filetypes_reset_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3816
|
+
|
|
3817
|
+
local_context_migrate_roots_v2_p = local_context_sub.add_parser("migrate-roots-v2", help="Plan or apply Local Memory roots v2 cleanup")
|
|
3818
|
+
local_context_migrate_roots_v2_p.add_argument("--apply", action="store_true", help="Apply cleanup. Omit for dry-run.")
|
|
3819
|
+
local_context_migrate_roots_v2_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3820
|
+
|
|
3762
3821
|
local_context_query_p = local_context_sub.add_parser("query", help="Query local memory evidence")
|
|
3763
3822
|
local_context_query_p.add_argument("query", help="Question or search phrase")
|
|
3764
3823
|
local_context_query_p.add_argument("--intent", default="answer", help="Intent label included with the query result")
|
|
@@ -4407,6 +4466,13 @@ def main():
|
|
|
4407
4466
|
local_context_exclusions_p.print_help()
|
|
4408
4467
|
return 0
|
|
4409
4468
|
return _local_context_exclusions(args)
|
|
4469
|
+
if args.local_context_command == "filetypes":
|
|
4470
|
+
if not args.local_context_filetypes_command:
|
|
4471
|
+
local_context_filetypes_p.print_help()
|
|
4472
|
+
return 0
|
|
4473
|
+
return _local_context_filetypes(args)
|
|
4474
|
+
if args.local_context_command == "migrate-roots-v2":
|
|
4475
|
+
return _local_context_migrate_roots_v2(args)
|
|
4410
4476
|
if args.local_context_command == "query":
|
|
4411
4477
|
return _local_context_query(args)
|
|
4412
4478
|
if args.local_context_command == "diagnostics":
|
package/src/db/_schema.py
CHANGED
|
@@ -1775,6 +1775,9 @@ def _m63_local_context_layer(conn):
|
|
|
1775
1775
|
display_path TEXT NOT NULL,
|
|
1776
1776
|
mode TEXT NOT NULL DEFAULT 'normal',
|
|
1777
1777
|
depth INTEGER NOT NULL DEFAULT 2,
|
|
1778
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
1779
|
+
remote INTEGER NOT NULL DEFAULT 0,
|
|
1780
|
+
seed_version INTEGER NOT NULL DEFAULT 1,
|
|
1778
1781
|
status TEXT NOT NULL DEFAULT 'active',
|
|
1779
1782
|
last_scan_at REAL,
|
|
1780
1783
|
created_at REAL NOT NULL,
|
|
@@ -1785,10 +1788,24 @@ def _m63_local_context_layer(conn):
|
|
|
1785
1788
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1786
1789
|
path TEXT NOT NULL UNIQUE,
|
|
1787
1790
|
display_path TEXT NOT NULL,
|
|
1791
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
1792
|
+
kind TEXT NOT NULL DEFAULT 'folder',
|
|
1788
1793
|
reason TEXT NOT NULL DEFAULT 'user',
|
|
1789
1794
|
created_at REAL NOT NULL
|
|
1790
1795
|
);
|
|
1791
1796
|
|
|
1797
|
+
CREATE TABLE IF NOT EXISTS local_index_file_type_rules (
|
|
1798
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1799
|
+
extension TEXT NOT NULL,
|
|
1800
|
+
action TEXT NOT NULL DEFAULT 'ignore',
|
|
1801
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
1802
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
1803
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
1804
|
+
created_at REAL NOT NULL,
|
|
1805
|
+
updated_at REAL NOT NULL,
|
|
1806
|
+
UNIQUE(extension, source)
|
|
1807
|
+
);
|
|
1808
|
+
|
|
1792
1809
|
CREATE TABLE IF NOT EXISTS local_index_jobs (
|
|
1793
1810
|
job_id TEXT PRIMARY KEY,
|
|
1794
1811
|
asset_id TEXT NOT NULL,
|
|
@@ -1940,6 +1957,12 @@ def _m63_local_context_layer(conn):
|
|
|
1940
1957
|
|
|
1941
1958
|
CREATE INDEX IF NOT EXISTS idx_local_index_roots_status
|
|
1942
1959
|
ON local_index_roots(status);
|
|
1960
|
+
CREATE INDEX IF NOT EXISTS idx_local_index_roots_source
|
|
1961
|
+
ON local_index_roots(source, status);
|
|
1962
|
+
CREATE INDEX IF NOT EXISTS idx_local_index_exclusions_source
|
|
1963
|
+
ON local_index_exclusions(source);
|
|
1964
|
+
CREATE INDEX IF NOT EXISTS idx_local_index_file_type_rules_ext
|
|
1965
|
+
ON local_index_file_type_rules(extension, source);
|
|
1943
1966
|
CREATE INDEX IF NOT EXISTS idx_local_index_jobs_status_priority
|
|
1944
1967
|
ON local_index_jobs(status, priority, created_at);
|
|
1945
1968
|
CREATE INDEX IF NOT EXISTS idx_local_index_jobs_asset
|
|
@@ -549,6 +549,111 @@ def check_python_runtime() -> DoctorCheck:
|
|
|
549
549
|
)
|
|
550
550
|
|
|
551
551
|
|
|
552
|
+
def _desktop_product_requested() -> bool:
|
|
553
|
+
try:
|
|
554
|
+
from product_mode import desktop_product_requested
|
|
555
|
+
return bool(desktop_product_requested())
|
|
556
|
+
except Exception:
|
|
557
|
+
return str(os.environ.get("NEXO_DESKTOP_MANAGED", "")).strip() == "1"
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _managed_venv_python_path() -> Path:
|
|
561
|
+
if sys.platform == "win32":
|
|
562
|
+
return NEXO_HOME / ".venv" / "Scripts" / "python.exe"
|
|
563
|
+
return NEXO_HOME / ".venv" / "bin" / "python3"
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _probe_python_version(python_bin: Path | str) -> tuple[int, int, int] | None:
|
|
567
|
+
try:
|
|
568
|
+
result = subprocess.run(
|
|
569
|
+
[str(python_bin), "-c", "import sys; print('.'.join(map(str, sys.version_info[:3])))"],
|
|
570
|
+
capture_output=True,
|
|
571
|
+
text=True,
|
|
572
|
+
timeout=15,
|
|
573
|
+
)
|
|
574
|
+
except Exception:
|
|
575
|
+
return None
|
|
576
|
+
if result.returncode != 0:
|
|
577
|
+
return None
|
|
578
|
+
import re
|
|
579
|
+
match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout or result.stderr or "")
|
|
580
|
+
if not match:
|
|
581
|
+
return None
|
|
582
|
+
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _managed_venv_version_supported(version: tuple[int, int, int] | None) -> bool:
|
|
586
|
+
if not version:
|
|
587
|
+
return False
|
|
588
|
+
if _desktop_product_requested():
|
|
589
|
+
return version[:2] == (3, 12)
|
|
590
|
+
return version >= (3, 10, 0)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _repair_managed_venv_python() -> bool:
|
|
594
|
+
try:
|
|
595
|
+
import auto_update
|
|
596
|
+
return bool(auto_update._ensure_runtime_venv(NEXO_HOME)) and bool(auto_update._reinstall_pip_deps())
|
|
597
|
+
except Exception:
|
|
598
|
+
return False
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def check_managed_venv_python(fix: bool = False) -> DoctorCheck:
|
|
602
|
+
"""Check the managed NEXO venv is compatible with bundled runtime deps."""
|
|
603
|
+
venv_python = _managed_venv_python_path()
|
|
604
|
+
if not venv_python.exists():
|
|
605
|
+
if not _desktop_product_requested():
|
|
606
|
+
return DoctorCheck(
|
|
607
|
+
id="boot.managed_venv_python",
|
|
608
|
+
tier="boot",
|
|
609
|
+
status="healthy",
|
|
610
|
+
severity="info",
|
|
611
|
+
summary="Managed Python venv not present yet",
|
|
612
|
+
evidence=[str(venv_python)],
|
|
613
|
+
)
|
|
614
|
+
return DoctorCheck(
|
|
615
|
+
id="boot.managed_venv_python",
|
|
616
|
+
tier="boot",
|
|
617
|
+
status="degraded",
|
|
618
|
+
severity="warn",
|
|
619
|
+
summary="Managed Python venv missing",
|
|
620
|
+
evidence=[str(venv_python)],
|
|
621
|
+
repair_plan=["Run nexo doctor --tier boot --fix or nexo update to recreate the managed venv"],
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
version = _probe_python_version(venv_python)
|
|
625
|
+
if _managed_venv_version_supported(version):
|
|
626
|
+
return DoctorCheck(
|
|
627
|
+
id="boot.managed_venv_python",
|
|
628
|
+
tier="boot",
|
|
629
|
+
status="healthy",
|
|
630
|
+
severity="info",
|
|
631
|
+
summary=f"Managed venv Python {'.'.join(map(str, version))}",
|
|
632
|
+
evidence=[str(venv_python)],
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
summary = "Managed venv Python is incompatible"
|
|
636
|
+
if version:
|
|
637
|
+
summary = f"Managed venv Python {'.'.join(map(str, version))} is incompatible"
|
|
638
|
+
if _desktop_product_requested():
|
|
639
|
+
summary += " — Desktop bundled wheels require Python 3.12"
|
|
640
|
+
if fix and _repair_managed_venv_python():
|
|
641
|
+
post = check_managed_venv_python(fix=False)
|
|
642
|
+
if post.status == "healthy":
|
|
643
|
+
post.fixed = True
|
|
644
|
+
post.summary += " (fixed)"
|
|
645
|
+
return post
|
|
646
|
+
return DoctorCheck(
|
|
647
|
+
id="boot.managed_venv_python",
|
|
648
|
+
tier="boot",
|
|
649
|
+
status="degraded",
|
|
650
|
+
severity="warn",
|
|
651
|
+
summary=summary,
|
|
652
|
+
evidence=[str(venv_python)],
|
|
653
|
+
repair_plan=["Run nexo doctor --tier boot --fix or nexo update to archive and recreate the managed venv"],
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
|
|
552
657
|
CRITICAL_CONFIG_FILES = (
|
|
553
658
|
("schedule.json", ("config", "schedule.json")),
|
|
554
659
|
("optionals.json", ("config", "optionals.json")),
|
|
@@ -803,6 +908,7 @@ def run_boot_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
|
803
908
|
safe_check(check_disk_space),
|
|
804
909
|
safe_check(check_wrapper_scripts),
|
|
805
910
|
safe_check(check_python_runtime),
|
|
911
|
+
safe_check(check_managed_venv_python, fix=fix),
|
|
806
912
|
safe_check(check_config_parse),
|
|
807
913
|
safe_check(check_core_dev_packaged_install),
|
|
808
914
|
safe_check(check_dashboard_desktop_contract),
|
|
@@ -17,17 +17,22 @@ from .api import (
|
|
|
17
17
|
get_asset,
|
|
18
18
|
get_neighbors,
|
|
19
19
|
list_exclusions,
|
|
20
|
+
list_file_type_rules,
|
|
20
21
|
list_roots,
|
|
21
22
|
local_index_hygiene,
|
|
23
|
+
migrate_roots_seed_v2,
|
|
22
24
|
model_status,
|
|
23
25
|
pause,
|
|
24
26
|
performance_config,
|
|
25
27
|
purge_asset,
|
|
26
28
|
reconcile_live_changes,
|
|
27
29
|
remove_exclusion,
|
|
30
|
+
remove_file_type_rule,
|
|
28
31
|
remove_root,
|
|
32
|
+
reset_file_type_rules,
|
|
29
33
|
resume,
|
|
30
34
|
run_once,
|
|
35
|
+
set_file_type_rule,
|
|
31
36
|
set_performance_profile,
|
|
32
37
|
status,
|
|
33
38
|
warmup_models,
|
|
@@ -45,17 +50,22 @@ __all__ = [
|
|
|
45
50
|
"get_asset",
|
|
46
51
|
"get_neighbors",
|
|
47
52
|
"list_exclusions",
|
|
53
|
+
"list_file_type_rules",
|
|
48
54
|
"list_roots",
|
|
49
55
|
"local_index_hygiene",
|
|
56
|
+
"migrate_roots_seed_v2",
|
|
50
57
|
"model_status",
|
|
51
58
|
"pause",
|
|
52
59
|
"performance_config",
|
|
53
60
|
"purge_asset",
|
|
54
61
|
"reconcile_live_changes",
|
|
55
62
|
"remove_exclusion",
|
|
63
|
+
"remove_file_type_rule",
|
|
56
64
|
"remove_root",
|
|
65
|
+
"reset_file_type_rules",
|
|
57
66
|
"resume",
|
|
58
67
|
"run_once",
|
|
68
|
+
"set_file_type_rule",
|
|
59
69
|
"set_performance_profile",
|
|
60
70
|
"status",
|
|
61
71
|
"warmup_models",
|