nexo-brain 5.3.7 → 5.3.9

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": "5.3.7",
3
+ "version": "5.3.9",
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 `5.3.4` closes the last packaged-runtime core/personal leak from the hook migration work: legacy hook aliases stay out of the personal bucket, `nexo update` removes those retired aliases when the canonical hooks already exist, and both `nexo` and `nexo chat` now show latest vs installed version at a glance.
21
+ Version `5.3.9` is the current packaged-runtime line: packaged updates now rebuild the core artifact manifest from the canonical npm package source instead of the live `~/.nexo/scripts` directory, so personal scripts stop being reclassified as core, portable user-data export keeps including them, and runtime doctor can recover personal LaunchAgent ownership cleanly after a bad `5.3.8` update.
22
22
 
23
23
  Start here:
24
24
  - [5-minute quickstart](docs/quickstart-5-minutes.md)
@@ -89,7 +89,7 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
89
89
  - when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
90
90
  - NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
91
91
 
92
- Version `5.3.7` closes the remaining packaged-runtime happy-path gap and finally exposes portable user-data migration commands: packaged `nexo update` now self-heals cron definitions and LaunchAgents after a successful npm bump, new `nexo export` / `nexo import` commands move operator data as a safe bundle instead of leaving that flow implicit, and runtime doctor now distinguishes tracked historical Codex drift from an actually broken runtime so cleaned installs stop staying red for stale transcript debt alone. Version `5.3.6` hardened the Claude Code bootstrap path and related runtime hygiene: managed client sync now writes the NEXO MCP server where current Claude Code actually reads it (`~/.claude.json`), script classification is stricter about core-vs-personal runtime artifacts, schedule status distinguishes genuinely running jobs from broken ones, and retroactive learnings stop opening keyword-only false positives outside their declared `applies_to` scope. Version `5.3.5` already keeps CLI version visibility honest right after `nexo update`: if the cached npm version lags behind the runtime you just installed, `nexo` / `nexo chat` now clamp `Latest` to the installed version and refresh the cache instead of showing a stale older release. Version `5.3.4` already cleaned up legacy core alias leakage and added the version-status banner. Version `5.3.3` closed the remaining packaged-runtime doctor mismatch: the built-in hourly backup helper is now inventoried as a core LaunchAgent, so clean installs no longer get a false unknown-LaunchAgent warning. Version `5.3.2` already hardened the runtime boundary by persisting which runtime scripts/hooks are core product artifacts, keeping `nexo scripts` from mixing those into the personal bucket, and migrating the legacy Claude Code heartbeat wrappers into managed core hooks.
92
+ Version `5.3.9` is the packaged core-artifact manifest heal for `5.3.8`: packaged updates now rebuild `runtime-core-artifacts.json` from the canonical npm package `src/` tree instead of scanning the live `~/.nexo/scripts` directory, script classification prefers that canonical packaged source when available, and runtime doctor syncs personal scripts before LaunchAgent inventory so personal automations recover cleanly instead of being mistaken for unknown core drift. Version `5.3.8` was the immediate packaged-migration hotfix for `5.3.7`: the installer/runtime migrator now discovers all top-level runtime Python modules from `src/` dynamically instead of relying on a manual allowlist, so new product surfaces like `nexo export` / `nexo import` actually arrive in `~/.nexo` after update instead of being present only in the published npm tarball. Version `5.3.7` closed the remaining packaged-runtime happy-path gap and finally exposed portable user-data migration commands: packaged `nexo update` now self-heals cron definitions and LaunchAgents after a successful npm bump, new `nexo export` / `nexo import` commands move operator data as a safe bundle instead of leaving that flow implicit, and runtime doctor now distinguishes tracked historical Codex drift from an actually broken runtime so cleaned installs stop staying red for stale transcript debt alone. Version `5.3.6` hardened the Claude Code bootstrap path and related runtime hygiene: managed client sync now writes the NEXO MCP server where current Claude Code actually reads it (`~/.claude.json`), script classification is stricter about core-vs-personal runtime artifacts, schedule status distinguishes genuinely running jobs from broken ones, and retroactive learnings stop opening keyword-only false positives outside their declared `applies_to` scope. Version `5.3.5` already keeps CLI version visibility honest right after `nexo update`: if the cached npm version lags behind the runtime you just installed, `nexo` / `nexo chat` now clamp `Latest` to the installed version and refresh the cache instead of showing a stale older release. Version `5.3.4` already cleaned up legacy core alias leakage and added the version-status banner. Version `5.3.3` closed the remaining packaged-runtime doctor mismatch: the built-in hourly backup helper is now inventoried as a core LaunchAgent, so clean installs no longer get a false unknown-LaunchAgent warning. Version `5.3.2` already hardened the runtime boundary by persisting which runtime scripts/hooks are core product artifacts, keeping `nexo scripts` from mixing those into the personal bucket, and migrating the legacy Claude Code heartbeat wrappers into managed core hooks.
93
93
 
94
94
  Version `5.3.1` normalizes packaged npm installs so they behave like packaged npm installs: `nexo update` now keeps the runtime anchored to `~/.nexo`, refreshes packaged bootstrap/client artifacts after upgrade, avoids repo-only release-artifact drift in installed runtimes, and keeps personal scripts on the canonical packaged path.
95
95
 
package/bin/nexo-brain.js CHANGED
@@ -120,8 +120,8 @@ function writeRuntimeCoreArtifactsManifest(nexoHome, srcDir) {
120
120
  }
121
121
  }
122
122
 
123
- function getCoreRuntimeFlatFiles() {
124
- return [
123
+ function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
124
+ const staticFiles = [
125
125
  "server.py",
126
126
  "plugin_loader.py",
127
127
  "knowledge_graph.py",
@@ -155,6 +155,11 @@ function getCoreRuntimeFlatFiles() {
155
155
  "runtime_power.py",
156
156
  "requirements.txt",
157
157
  ];
158
+ const discoveredRootModules = fs.existsSync(srcDir)
159
+ ? fs.readdirSync(srcDir)
160
+ .filter((name) => name.endsWith(".py") && fs.statSync(path.join(srcDir, name)).isFile())
161
+ : [];
162
+ return [...new Set([...staticFiles, ...discoveredRootModules])];
158
163
  }
159
164
 
160
165
  function getCoreRuntimePackages() {
@@ -1480,7 +1485,7 @@ async function main() {
1480
1485
  log(" Hooks updated.");
1481
1486
 
1482
1487
  // Update core Python files (flat .py files in src/)
1483
- const coreFlatFiles = getCoreRuntimeFlatFiles();
1488
+ const coreFlatFiles = getCoreRuntimeFlatFiles(srcDir);
1484
1489
  coreFlatFiles.forEach((f) => {
1485
1490
  const src = path.join(srcDir, f);
1486
1491
  if (fs.existsSync(src)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.3.7",
3
+ "version": "5.3.9",
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",
@@ -1621,6 +1621,13 @@ def check_launchagent_inventory() -> DoctorCheck:
1621
1621
  summary="No com.nexo LaunchAgents discovered on this Mac",
1622
1622
  )
1623
1623
 
1624
+ try:
1625
+ from script_registry import sync_personal_scripts
1626
+
1627
+ sync_personal_scripts(prune_missing=True)
1628
+ except Exception:
1629
+ pass
1630
+
1624
1631
  known_ids = _known_nexo_launchagent_ids()
1625
1632
  unknown_ids = sorted(actual_ids - known_ids)
1626
1633
  if not unknown_ids:
@@ -75,6 +75,14 @@ def _find_npm_pkg_src() -> Path | None:
75
75
  pass
76
76
  return None
77
77
 
78
+
79
+ def _core_artifact_source_dir() -> Path | None:
80
+ """Return the canonical source directory for packaged core artifacts."""
81
+ if _PACKAGED_INSTALL:
82
+ return _find_npm_pkg_src()
83
+ return SRC_DIR
84
+
85
+
78
86
  def _is_git_repo() -> bool:
79
87
  """Check if REPO_DIR is a valid git repository."""
80
88
  return (REPO_DIR / ".git").exists() or (REPO_DIR / ".git").is_file()
@@ -83,7 +91,11 @@ def _is_git_repo() -> bool:
83
91
  def _refresh_installed_manifest():
84
92
  """Refresh packaged crons and persist the runtime core-artifacts manifest."""
85
93
  try:
86
- src_crons = SRC_DIR / "crons"
94
+ artifact_src = _core_artifact_source_dir()
95
+ if artifact_src is None:
96
+ return
97
+
98
+ src_crons = artifact_src / "crons"
87
99
  dst_crons = NEXO_HOME / "crons"
88
100
  if src_crons.exists():
89
101
  dst_crons.mkdir(parents=True, exist_ok=True)
@@ -98,13 +110,13 @@ def _refresh_installed_manifest():
98
110
  payload = {
99
111
  "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
100
112
  "script_names": sorted(
101
- f.name for f in (SRC_DIR / "scripts").iterdir()
113
+ f.name for f in (artifact_src / "scripts").iterdir()
102
114
  if f.is_file()
103
- ) if (SRC_DIR / "scripts").is_dir() else [],
115
+ ) if (artifact_src / "scripts").is_dir() else [],
104
116
  "hook_names": sorted(
105
- f.name for f in (SRC_DIR / "hooks").iterdir()
117
+ f.name for f in (artifact_src / "hooks").iterdir()
106
118
  if f.is_file()
107
- ) if (SRC_DIR / "hooks").is_dir() else [],
119
+ ) if (artifact_src / "hooks").is_dir() else [],
108
120
  }
109
121
  (config_dir / "runtime-core-artifacts.json").write_text(
110
122
  json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
@@ -161,10 +161,36 @@ def _add_filenames_from_dir(names: set[str], directory: Path, *, skip_if_scripts
161
161
  names.add(item.name)
162
162
 
163
163
 
164
+ def _find_packaged_core_source_dir() -> Path | None:
165
+ repo_root = NEXO_CODE.parent
166
+ if (repo_root / ".git").exists() or (repo_root / ".git").is_file():
167
+ return None
168
+
169
+ with contextlib.suppress(Exception):
170
+ result = subprocess.run(
171
+ ["npm", "root", "-g"],
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=10,
175
+ )
176
+ if result.returncode == 0:
177
+ candidate = Path(result.stdout.strip()) / "nexo-brain" / "src"
178
+ if candidate.is_dir():
179
+ return candidate
180
+ return None
181
+
182
+
164
183
  def load_core_script_names() -> set[str]:
165
184
  """Load every core-managed runtime artifact name that must never be treated as personal."""
166
185
  names: set[str] = set()
167
- for manifest_path in [NEXO_CODE / "crons" / "manifest.json", NEXO_HOME / "crons" / "manifest.json"]:
186
+ packaged_src = _find_packaged_core_source_dir()
187
+
188
+ manifest_candidates = []
189
+ if packaged_src is not None:
190
+ manifest_candidates.append(packaged_src / "crons" / "manifest.json")
191
+ manifest_candidates.extend([NEXO_CODE / "crons" / "manifest.json", NEXO_HOME / "crons" / "manifest.json"])
192
+
193
+ for manifest_path in manifest_candidates:
168
194
  if manifest_path.exists():
169
195
  try:
170
196
  data = json.loads(manifest_path.read_text())
@@ -176,17 +202,21 @@ def load_core_script_names() -> set[str]:
176
202
  except Exception:
177
203
  continue
178
204
 
179
- for artifact_path in (
180
- NEXO_HOME / "config" / "runtime-core-artifacts.json",
181
- NEXO_CODE / "config" / "runtime-core-artifacts.json",
182
- NEXO_CODE.parent / "config" / "runtime-core-artifacts.json",
183
- ):
184
- if artifact_path.exists():
185
- _add_runtime_artifact_names(names, artifact_path)
186
-
187
- _add_filenames_from_dir(names, NEXO_HOME / "hooks")
188
- _add_filenames_from_dir(names, NEXO_CODE / "hooks")
189
- _add_filenames_from_dir(names, NEXO_CODE / "scripts", skip_if_scripts_dir=True)
205
+ if packaged_src is not None:
206
+ _add_filenames_from_dir(names, packaged_src / "hooks")
207
+ _add_filenames_from_dir(names, packaged_src / "scripts")
208
+ else:
209
+ for artifact_path in (
210
+ NEXO_HOME / "config" / "runtime-core-artifacts.json",
211
+ NEXO_CODE / "config" / "runtime-core-artifacts.json",
212
+ NEXO_CODE.parent / "config" / "runtime-core-artifacts.json",
213
+ ):
214
+ if artifact_path.exists():
215
+ _add_runtime_artifact_names(names, artifact_path)
216
+
217
+ _add_filenames_from_dir(names, NEXO_HOME / "hooks")
218
+ _add_filenames_from_dir(names, NEXO_CODE / "hooks")
219
+ _add_filenames_from_dir(names, NEXO_CODE / "scripts", skip_if_scripts_dir=True)
190
220
 
191
221
  for legacy_name, canonical_name in _LEGACY_CORE_SCRIPT_ALIASES.items():
192
222
  if canonical_name in names:
@@ -95,12 +95,32 @@ def _copy_file_if_present(src: Path, dest: Path) -> bool:
95
95
 
96
96
 
97
97
  def _safe_extract(archive_path: Path, dest_dir: Path) -> None:
98
+ resolved_dest = dest_dir.resolve()
98
99
  with tarfile.open(archive_path, "r:*") as tar:
99
- for member in tar.getmembers():
100
+ members = tar.getmembers()
101
+ for member in members:
100
102
  target = (dest_dir / member.name).resolve()
101
- if not str(target).startswith(str(dest_dir.resolve())):
103
+ if target != resolved_dest and resolved_dest not in target.parents:
102
104
  raise ValueError(f"archive path escapes destination: {member.name}")
103
- tar.extractall(dest_dir)
105
+ if member.issym() or member.islnk():
106
+ raise ValueError(f"archive contains unsupported link member: {member.name}")
107
+
108
+ for member in members:
109
+ target = (dest_dir / member.name).resolve()
110
+ if member.isdir():
111
+ target.mkdir(parents=True, exist_ok=True)
112
+ target.chmod(member.mode & 0o777)
113
+ continue
114
+ if not member.isfile():
115
+ raise ValueError(f"archive contains unsupported member type: {member.name}")
116
+
117
+ target.parent.mkdir(parents=True, exist_ok=True)
118
+ extracted = tar.extractfile(member)
119
+ if extracted is None:
120
+ raise ValueError(f"archive member could not be read: {member.name}")
121
+ with extracted, target.open("wb") as handle:
122
+ shutil.copyfileobj(extracted, handle)
123
+ target.chmod(member.mode & 0o777)
104
124
 
105
125
 
106
126
  def _load_personal_scripts() -> tuple[list[dict], list[dict]]:
@@ -298,5 +318,11 @@ def import_user_bundle(bundle_path: str) -> dict:
298
318
  "restored": restored,
299
319
  "reconciled": reconcile_result,
300
320
  }
321
+ except Exception as exc:
322
+ return {
323
+ "ok": False,
324
+ "error": str(exc),
325
+ "safety_backup": str(safety_backup),
326
+ }
301
327
  finally:
302
328
  shutil.rmtree(stage_dir, ignore_errors=True)