nexo-brain 7.1.0 → 7.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
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,7 @@
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.1.0` is the current packaged-runtime line. It closes the post-F0.6 runtime contract: `~/.nexo/core` is now the canonical shipped code root, layout healers re-run automatically after sync/update, Desktop consumes a Brain-generated Guardian runtime snapshot instead of stale manual lists, the core automation surface now officially includes `email-monitor`, `followup-runner`, and `morning-agent`, and the local classifier baseline auto-installs on fresh installs / `nexo update` unless the operator explicitly opts out with `NEXO_LOCAL_CLASSIFIER=off`. The companion NEXO Desktop client (v0.22.0, closed-source distributed separately) turns that same contract into a closed bootstrap/login/product flow.
21
+ Version `7.1.1` is the current packaged-runtime line. It hotfixes the post-F0.6 updater path after `7.1.0`: packaged installs no longer confuse `~/.nexo/core` with a mutable source repo, and source-sync/update flows now replace compatibility shims safely instead of failing on `db` / `cognitive` / `skills-core` / root-file conflicts. The companion NEXO Desktop client (v0.22.1, closed-source distributed separately) embeds the same hotfix so fresh installs and in-app repairs start from the corrected updater baseline.
22
22
 
23
23
  Previously in `7.0.1`: hotfix over v7.0.0 (db._core.DB_PATH was only caller still hardcoded to legacy ~/.nexo/data/nexo.db; every shared-DB command silently returned empty results post-migration). Previously in `7.0.0`: **BREAKING — Plan Consolidado fase F0.6**: physical separation of the runtime tree into `~/.nexo/{core,personal,runtime}/`. The flat layout (`~/.nexo/scripts/`, `brain/`, `data/`, `operations/`, ...) is gone. Operators on v6.x are auto-migrated on first `nexo update`; fresh installs land directly in the new tree. New `paths.py` helpers are transition-aware.
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.1.0",
3
+ "version": "7.1.1",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain \u2014 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",
@@ -3612,6 +3612,23 @@ def _resolve_sync_source() -> tuple[Path | None, Path | None]:
3612
3612
  return candidate
3613
3613
  return None
3614
3614
 
3615
+ try:
3616
+ runtime_core = (dest / "core").resolve()
3617
+ code_resolved = NEXO_CODE.resolve()
3618
+ except Exception:
3619
+ runtime_core = dest / "core"
3620
+ code_resolved = NEXO_CODE
3621
+
3622
+ # Packaged/runtime-only installs resolve the launcher to ``~/.nexo/core``.
3623
+ # Those must use the packaged updater path instead of treating the managed
3624
+ # runtime itself as a mutable source repository. Only a recorded external
3625
+ # source repo in ``version.json`` should reactivate source-sync mode.
3626
+ if code_resolved == runtime_core:
3627
+ version_source = _runtime_version_source()
3628
+ if version_source:
3629
+ return version_source / "src", version_source
3630
+ return None, None
3631
+
3615
3632
  try:
3616
3633
  same_as_runtime = NEXO_CODE.resolve() == dest.resolve()
3617
3634
  except Exception:
@@ -3798,6 +3815,31 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
3798
3815
  return str(backup_dir)
3799
3816
 
3800
3817
 
3818
+ def _remove_runtime_copy_target(target: Path) -> None:
3819
+ """Remove a runtime copy destination, handling symlinks explicitly.
3820
+
3821
+ F0.6 installs keep compatibility shims such as ``~/.nexo/db ->
3822
+ ~/.nexo/core/db``. ``shutil.rmtree(..., ignore_errors=True)`` does not
3823
+ remove those symlinks, which makes subsequent ``copytree(...)`` calls fail
3824
+ with ``FileExistsError`` during ``nexo update``. This helper treats
3825
+ symlink/file targets differently from real directories so runtime sync and
3826
+ rollback can replace shim paths safely.
3827
+ """
3828
+ import shutil
3829
+
3830
+ try:
3831
+ if target.is_symlink() or target.is_file():
3832
+ target.unlink()
3833
+ return
3834
+ if target.is_dir():
3835
+ shutil.rmtree(str(target), ignore_errors=True)
3836
+ return
3837
+ if target.exists():
3838
+ target.unlink()
3839
+ except FileNotFoundError:
3840
+ return
3841
+
3842
+
3801
3843
  def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
3802
3844
  import shutil
3803
3845
 
@@ -3807,11 +3849,11 @@ def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
3807
3849
  for item in bdir.iterdir():
3808
3850
  target = dest / item.name
3809
3851
  if item.is_dir():
3810
- if target.exists():
3811
- shutil.rmtree(target, ignore_errors=True)
3852
+ _remove_runtime_copy_target(target)
3812
3853
  shutil.copytree(str(item), str(target))
3813
3854
  else:
3814
3855
  target.parent.mkdir(parents=True, exist_ok=True)
3856
+ _remove_runtime_copy_target(target)
3815
3857
  shutil.copy2(str(item), str(target))
3816
3858
 
3817
3859
 
@@ -3834,8 +3876,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
3834
3876
  pkg_src = src_dir / pkg
3835
3877
  pkg_dest = dest / pkg
3836
3878
  if pkg_src.is_dir():
3837
- if pkg_dest.exists():
3838
- shutil.rmtree(str(pkg_dest), ignore_errors=True)
3879
+ _remove_runtime_copy_target(pkg_dest)
3839
3880
  shutil.copytree(
3840
3881
  str(pkg_src),
3841
3882
  str(pkg_dest),
@@ -3847,30 +3888,37 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
3847
3888
  for name in flat_files:
3848
3889
  src_file = src_dir / name
3849
3890
  if src_file.is_file():
3850
- shutil.copy2(str(src_file), str(dest / name))
3891
+ target = dest / name
3892
+ _remove_runtime_copy_target(target)
3893
+ shutil.copy2(str(src_file), str(target))
3851
3894
  copied_files += 1
3852
3895
 
3853
3896
  _emit_progress(progress_fn, "Copying plugin modules...")
3854
3897
  plugins_src = src_dir / "plugins"
3855
3898
  plugins_dest = dest / "plugins"
3856
3899
  if plugins_src.is_dir():
3900
+ if plugins_dest.is_symlink():
3901
+ _remove_runtime_copy_target(plugins_dest)
3857
3902
  plugins_dest.mkdir(parents=True, exist_ok=True)
3858
3903
  for item in plugins_src.iterdir():
3859
3904
  if item.is_file() and item.suffix == ".py" and not is_duplicate_artifact_name(item):
3860
- shutil.copy2(str(item), str(plugins_dest / item.name))
3905
+ target = plugins_dest / item.name
3906
+ _remove_runtime_copy_target(target)
3907
+ shutil.copy2(str(item), str(target))
3861
3908
 
3862
3909
  _emit_progress(progress_fn, "Copying scripts...")
3863
3910
  scripts_src = src_dir / "scripts"
3864
3911
  scripts_dest = dest / "scripts"
3865
3912
  if scripts_src.is_dir():
3913
+ if scripts_dest.is_symlink():
3914
+ _remove_runtime_copy_target(scripts_dest)
3866
3915
  scripts_dest.mkdir(parents=True, exist_ok=True)
3867
3916
  for item in scripts_src.iterdir():
3868
3917
  if item.name == "__pycache__" or item.name.startswith(".") or is_duplicate_artifact_name(item):
3869
3918
  continue
3870
3919
  dst = scripts_dest / item.name
3871
3920
  if item.is_dir():
3872
- if dst.exists():
3873
- shutil.rmtree(str(dst), ignore_errors=True)
3921
+ _remove_runtime_copy_target(dst)
3874
3922
  shutil.copytree(str(item), str(dst), ignore=_runtime_copy_ignore())
3875
3923
  elif item.is_file():
3876
3924
  existing_class = installed_script_classes.get(item.name, "")
@@ -3899,25 +3947,37 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
3899
3947
  templates_src = repo_dir / "templates"
3900
3948
  templates_dest = dest / "templates"
3901
3949
  if templates_src.is_dir():
3950
+ if templates_dest.is_symlink():
3951
+ _remove_runtime_copy_target(templates_dest)
3902
3952
  templates_dest.mkdir(parents=True, exist_ok=True)
3903
3953
  for item in templates_src.iterdir():
3904
3954
  if item.name == "__pycache__" or is_duplicate_artifact_name(item):
3905
3955
  continue
3906
3956
  if item.is_file():
3907
- shutil.copy2(str(item), str(templates_dest / item.name))
3957
+ target = templates_dest / item.name
3958
+ _remove_runtime_copy_target(target)
3959
+ shutil.copy2(str(item), str(target))
3908
3960
  elif item.is_dir():
3909
3961
  sub_dest = templates_dest / item.name
3962
+ if sub_dest.is_symlink():
3963
+ _remove_runtime_copy_target(sub_dest)
3910
3964
  sub_dest.mkdir(parents=True, exist_ok=True)
3911
3965
  for sub in item.iterdir():
3912
3966
  if sub.is_file() and not is_duplicate_artifact_name(sub):
3913
- shutil.copy2(str(sub), str(sub_dest / sub.name))
3967
+ target = sub_dest / sub.name
3968
+ _remove_runtime_copy_target(target)
3969
+ shutil.copy2(str(sub), str(target))
3914
3970
 
3915
3971
  package_json = repo_dir / "package.json"
3916
3972
  if package_json.is_file():
3917
- shutil.copy2(str(package_json), str(dest / "package.json"))
3973
+ package_target = dest / "package.json"
3974
+ _remove_runtime_copy_target(package_target)
3975
+ shutil.copy2(str(package_json), str(package_target))
3918
3976
  try:
3919
3977
  pkg = json.loads(package_json.read_text())
3920
- (dest / "version.json").write_text(json.dumps({
3978
+ version_target = dest / "version.json"
3979
+ _remove_runtime_copy_target(version_target)
3980
+ version_target.write_text(json.dumps({
3921
3981
  "version": pkg.get("version", "?"),
3922
3982
  "source": str(repo_dir),
3923
3983
  }, indent=2))
@@ -3928,8 +3988,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
3928
3988
  skills_src = src_dir / "skills"
3929
3989
  skills_dest = dest / "skills-core"
3930
3990
  if skills_src.is_dir():
3931
- if skills_dest.exists():
3932
- shutil.rmtree(str(skills_dest), ignore_errors=True)
3991
+ _remove_runtime_copy_target(skills_dest)
3933
3992
  shutil.copytree(str(skills_src), str(skills_dest), ignore=_runtime_copy_ignore())
3934
3993
 
3935
3994
  bin_dir = dest / "bin"