nexo-brain 3.0.0 → 3.0.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": "3.0.0",
3
+ "version": "3.0.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
@@ -50,7 +50,7 @@ That means NEXO now manages not only the shared runtime and MCP wiring, but also
50
50
 
51
51
  Versions `2.6.14` through `2.7.0` established the practical shared-brain baseline: managed Claude/Codex bootstrap, Codex config sync, transcript-aware Deep Sleep, 60-day long-horizon analysis, weekly/monthly summary artifacts, retrieval auto-mode, and the first measured engineering loop.
52
52
 
53
- Version `3.0.0` closes the next execution gap:
53
+ Versions `3.0.0` and `3.0.1` close the next execution gap:
54
54
 
55
55
  - protocol discipline is now a runtime contract, not just instructions:
56
56
  - `nexo_task_open`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
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",
@@ -9,9 +9,13 @@ import shutil
9
9
  import subprocess
10
10
  import tempfile
11
11
  import time
12
- import tomllib
13
12
  from pathlib import Path
14
13
 
14
+ try:
15
+ import tomllib
16
+ except ModuleNotFoundError: # Python < 3.11
17
+ import tomli as tomllib
18
+
15
19
  from client_preferences import (
16
20
  BACKEND_NONE,
17
21
  CLIENT_CLAUDE_CODE,
@@ -5,9 +5,13 @@ from __future__ import annotations
5
5
  import os
6
6
  import shutil
7
7
  import sys
8
- import tomllib
9
8
  from pathlib import Path
10
9
 
10
+ try:
11
+ import tomllib
12
+ except ModuleNotFoundError: # Python < 3.11
13
+ import tomli as tomllib
14
+
11
15
  from runtime_power import load_schedule_config, save_schedule_config
12
16
 
13
17
 
@@ -10,9 +10,13 @@ import shlex
10
10
  import shutil
11
11
  import subprocess
12
12
  import sys
13
- import tomllib
14
13
  from pathlib import Path
15
14
 
15
+ try:
16
+ import tomllib
17
+ except ModuleNotFoundError: # Python < 3.11
18
+ import tomli as tomllib
19
+
16
20
  from bootstrap_docs import sync_client_bootstrap
17
21
 
18
22
  try:
@@ -30,7 +30,7 @@ if _PARENT not in sys.path:
30
30
 
31
31
  from agent_runner import AgentRunnerError, build_followup_terminal_shell_command
32
32
 
33
- app = FastAPI(title="NEXO Brain Dashboard", version="3.0.0")
33
+ app = FastAPI(title="NEXO Brain Dashboard", version="3.0.1")
34
34
 
35
35
  TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
36
36
  STATIC_DIR = Path(__file__).resolve().parent / "static"
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import json
5
5
  import secrets
6
6
  import time
7
- from datetime import UTC, datetime
7
+ from datetime import datetime, timezone
8
8
 
9
9
  from db._core import get_db
10
10
 
@@ -48,7 +48,7 @@ def _workflow_goal_id() -> str:
48
48
 
49
49
 
50
50
  def _now_sql() -> str:
51
- return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
51
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
52
52
 
53
53
 
54
54
  def _as_json(value, default):
@@ -148,37 +148,63 @@ def check_python_runtime() -> DoctorCheck:
148
148
  )
149
149
 
150
150
 
151
+ CRITICAL_CONFIG_FILES = (
152
+ ("schedule.json", ("config", "schedule.json")),
153
+ ("optionals.json", ("config", "optionals.json")),
154
+ ("crons/manifest.json", ("crons", "manifest.json")),
155
+ )
156
+
157
+
151
158
  def check_config_parse() -> DoctorCheck:
152
- """Check schedule.json parses correctly if present."""
153
- schedule_file = NEXO_HOME / "config" / "schedule.json"
154
- if not schedule_file.exists():
159
+ """Validate that critical JSON config files parse correctly."""
160
+ import json
161
+
162
+ errors: list[str] = []
163
+ checked: list[str] = []
164
+
165
+ for label, relative in CRITICAL_CONFIG_FILES:
166
+ path = NEXO_HOME.joinpath(*relative)
167
+ if not path.exists():
168
+ continue
169
+ try:
170
+ data = json.loads(path.read_text())
171
+ except Exception as exc:
172
+ errors.append(f"{label}: {exc}")
173
+ continue
174
+ if not isinstance(data, dict):
175
+ errors.append(f"{label}: expected JSON object, got {type(data).__name__}")
176
+ continue
177
+ checked.append(label)
178
+
179
+ if errors:
155
180
  return DoctorCheck(
156
181
  id="boot.config_parse",
157
182
  tier="boot",
158
- status="healthy",
159
- severity="info",
160
- summary="No schedule.json (using defaults)",
183
+ status="degraded",
184
+ severity="warn",
185
+ summary=f"{len(errors)} config file parse error" + ("s" if len(errors) != 1 else ""),
186
+ evidence=errors,
187
+ repair_plan=["Fix JSON syntax in the listed config files, or delete them to fall back to defaults"],
161
188
  )
162
- try:
163
- import json
164
- json.loads(schedule_file.read_text())
189
+
190
+ if not checked:
165
191
  return DoctorCheck(
166
192
  id="boot.config_parse",
167
193
  tier="boot",
168
194
  status="healthy",
169
195
  severity="info",
170
- summary="schedule.json parses OK",
171
- )
172
- except Exception as e:
173
- return DoctorCheck(
174
- id="boot.config_parse",
175
- tier="boot",
176
- status="degraded",
177
- severity="warn",
178
- summary=f"schedule.json parse error: {e}",
179
- repair_plan=["Fix JSON syntax in schedule.json or delete to use defaults"],
196
+ summary="No config files present (using defaults)",
180
197
  )
181
198
 
199
+ return DoctorCheck(
200
+ id="boot.config_parse",
201
+ tier="boot",
202
+ status="healthy",
203
+ severity="info",
204
+ summary=f"{len(checked)} config file" + ("s" if len(checked) != 1 else "") + " parse OK",
205
+ evidence=checked,
206
+ )
207
+
182
208
 
183
209
  def run_boot_checks(fix: bool = False) -> list[DoctorCheck]:
184
210
  """Run all boot-tier checks."""
@@ -12,9 +12,13 @@ import sqlite3
12
12
  import subprocess
13
13
  import sys
14
14
  import time
15
- import tomllib
16
15
  from pathlib import Path
17
16
 
17
+ try:
18
+ import tomllib
19
+ except ModuleNotFoundError: # Python < 3.11
20
+ import tomli as tomllib
21
+
18
22
  from client_preferences import (
19
23
  detect_installed_clients,
20
24
  normalize_client_preferences,
@@ -738,7 +742,7 @@ def _parse_timestamp(value: str) -> dt.datetime | None:
738
742
  except ValueError:
739
743
  return None
740
744
  if parsed.tzinfo is None:
741
- parsed = parsed.replace(tzinfo=dt.UTC)
745
+ parsed = parsed.replace(tzinfo=dt.timezone.utc)
742
746
  return parsed
743
747
 
744
748
 
@@ -2470,7 +2474,7 @@ def check_state_watchers() -> DoctorCheck:
2470
2474
  generated_dt = dt.datetime.fromisoformat(str(generated_at).replace("Z", "+00:00"))
2471
2475
  except Exception:
2472
2476
  generated_dt = None
2473
- if not generated_dt or (dt.datetime.now(dt.UTC) - generated_dt).total_seconds() > 36 * 3600:
2477
+ if not generated_dt or (dt.datetime.now(dt.timezone.utc) - generated_dt).total_seconds() > 36 * 3600:
2474
2478
  status = "degraded"
2475
2479
  severity = "warn"
2476
2480
  repair_plan.append("Refresh state watchers daily so repo/API/expiry drift stays explicit")
@@ -11,3 +11,4 @@ fastapi
11
11
  uvicorn
12
12
  pydantic
13
13
  jinja2
14
+ tomli; python_version < "3.11"
@@ -5,7 +5,7 @@ import json
5
5
  import os
6
6
  import sqlite3
7
7
  import subprocess
8
- from datetime import UTC, datetime, timedelta
8
+ from datetime import datetime, timedelta, timezone
9
9
  from pathlib import Path
10
10
  from urllib import error, request
11
11
 
@@ -30,7 +30,7 @@ def _manifest_file() -> Path:
30
30
 
31
31
 
32
32
  def _now_iso() -> str:
33
- return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
33
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
34
34
 
35
35
 
36
36
  def _parse_dt(value: str | None) -> datetime | None:
@@ -41,13 +41,13 @@ def _parse_dt(value: str | None) -> datetime | None:
41
41
  for candidate in (normalized, normalized + "T00:00:00+00:00"):
42
42
  try:
43
43
  parsed = datetime.fromisoformat(candidate)
44
- return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
44
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
45
45
  except Exception:
46
46
  continue
47
47
  for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
48
48
  try:
49
49
  parsed = datetime.strptime(text, fmt)
50
- return parsed.replace(tzinfo=UTC)
50
+ return parsed.replace(tzinfo=timezone.utc)
51
51
  except Exception:
52
52
  continue
53
53
  return None
@@ -150,7 +150,7 @@ def _evaluate_cron_drift(watcher: dict) -> dict:
150
150
  last_run = _load_last_cron_run(cron_id)
151
151
  if not last_run:
152
152
  return {"health": "critical", "summary": f"cron {cron_id} has never run", "evidence": [f"threshold_seconds={threshold}"]}
153
- age = (datetime.now(UTC) - last_run).total_seconds()
153
+ age = (datetime.now(timezone.utc) - last_run).total_seconds()
154
154
  if age > threshold * 2:
155
155
  health = "critical"
156
156
  elif age > threshold:
@@ -218,7 +218,7 @@ def _evaluate_expiry(watcher: dict) -> dict:
218
218
  return {"health": "critical", "summary": f"expiry watcher missing due_at: {watcher.get('watcher_id')}", "evidence": []}
219
219
  warn_days = int(config.get("warn_days") or 21)
220
220
  critical_days = int(config.get("critical_days") or 7)
221
- remaining = due_at - datetime.now(UTC)
221
+ remaining = due_at - datetime.now(timezone.utc)
222
222
  days = remaining.total_seconds() / 86400
223
223
  if days <= critical_days:
224
224
  health = "critical"
@@ -6,7 +6,7 @@ import os
6
6
  import time
7
7
  import secrets
8
8
  import threading
9
- from datetime import datetime, UTC
9
+ from datetime import datetime, timezone
10
10
  from pathlib import Path
11
11
  from db import (
12
12
  register_session, update_session, complete_session,
@@ -133,7 +133,7 @@ def _session_portability_bundle(sid: str = "") -> dict:
133
133
  ]
134
134
  return {
135
135
  "ok": True,
136
- "generated_at": datetime.now(UTC).isoformat(),
136
+ "generated_at": datetime.now(timezone.utc).isoformat(),
137
137
  "session": {
138
138
  "sid": session_id,
139
139
  "task": session_row["task"],