nexo-brain 7.17.4 → 7.17.6

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.17.4",
3
+ "version": "7.17.6",
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.17.4` is the current packaged-runtime line. Corrective patch over v7.17.3 - automation runners now keep full NEXO discipline for real background agents while strict JSON children stay clean, and runtime doctor/metrics expose caller coverage and Guardian injection telemetry instead of hiding blind spots.
21
+ Version `7.17.6` is the current packaged-runtime line. Patch release over v7.17.5 - cron health diagnostics are clearer for macOS TCC approval, and catch-up fallback executions now stay visible in `cron_runs` even on legacy or partially migrated runtimes.
22
+
23
+ Previously in `7.17.4`: corrective patch over v7.17.3 - automation runners now keep full NEXO discipline for real background agents while strict JSON children stay clean, and runtime doctor/metrics expose caller coverage and Guardian injection telemetry instead of hiding blind spots.
22
24
 
23
25
  Previously in `7.17.3`: corrective patch over v7.17.2 - standalone Brain install/update no longer aborts when the Desktop-only `qwen3-0.6b-q4-local-presence` model is not bundled or already cached locally. Required Brain warmups stay strict; only the optional local-presence GGUF now degrades cleanly.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.17.4",
3
+ "version": "7.17.6",
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/cli.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  Entry points:
5
5
  nexo chat [PATH]
6
+ nexo --version [--json]
6
7
  nexo export [PATH] [--json]
7
8
  nexo import-inspect PATH [--json]
8
9
  nexo import PATH [--json]
@@ -233,17 +234,41 @@ def _version_sort_key(raw: str) -> tuple[tuple[int, ...], int, str]:
233
234
  return (tuple(parts), 1 if not suffix else 0, suffix)
234
235
 
235
236
 
236
- def _version_status_line() -> str:
237
+ def _version_status_payload() -> dict:
237
238
  installed = _get_version()
238
239
  latest = _load_latest_version_cache()
240
+ latest_source = "cache" if latest else ""
239
241
  if latest is None and _should_refresh_latest_version():
240
242
  latest = _fetch_latest_version()
243
+ latest_source = "npm" if latest else ""
241
244
  if latest and installed and _version_sort_key(latest) < _version_sort_key(installed):
242
245
  latest = installed
246
+ latest_source = "installed"
243
247
  try:
244
248
  _save_latest_version_cache(installed)
245
249
  except Exception:
246
250
  pass
251
+ has_update = bool(
252
+ latest
253
+ and installed
254
+ and _version_sort_key(latest) > _version_sort_key(installed)
255
+ )
256
+ return {
257
+ "ok": True,
258
+ "name": "nexo",
259
+ "package": LATEST_NPM_PACKAGE,
260
+ "installed": installed,
261
+ "latest": latest or "",
262
+ "hasUpdate": has_update,
263
+ "unknown": not bool(installed and latest),
264
+ "latestSource": latest_source,
265
+ }
266
+
267
+
268
+ def _version_status_line(payload: dict | None = None) -> str:
269
+ payload = payload or _version_status_payload()
270
+ installed = str(payload.get("installed") or "?").strip()
271
+ latest = str(payload.get("latest") or "").strip()
247
272
  if latest:
248
273
  return f"NEXO Latest: v{latest} | Installed: v{installed}"
249
274
  return f"NEXO Installed: v{installed}"
@@ -2823,6 +2848,7 @@ def main():
2823
2848
  parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI", add_help=False)
2824
2849
  parser.add_argument("-h", "--help", action="store_true", help="Show help")
2825
2850
  parser.add_argument("-v", "--version", action="store_true", help="Show version")
2851
+ parser.add_argument("--json", action="store_true", help="JSON output for --version")
2826
2852
  sub = parser.add_subparsers(dest="command")
2827
2853
 
2828
2854
  # -- email (Plan F1 — interactive wizard for email accounts) --
@@ -3408,7 +3434,12 @@ def main():
3408
3434
  _print_help()
3409
3435
  return 0
3410
3436
  if args.version:
3411
- print(f"nexo v{_get_version()}")
3437
+ payload = _version_status_payload()
3438
+ if args.json:
3439
+ print(json.dumps(payload, ensure_ascii=False))
3440
+ else:
3441
+ print(f"nexo v{payload.get('installed') or _get_version()}")
3442
+ print(_version_status_line(payload))
3412
3443
  return 0
3413
3444
 
3414
3445
  if args.command == "email":
@@ -8,9 +8,10 @@ Legacy .catchup-state.json is now only a fallback for pre-wrapper history.
8
8
  import fcntl
9
9
  import json
10
10
  import os
11
+ import sqlite3
11
12
  import subprocess
12
13
  import sys
13
- from datetime import datetime
14
+ from datetime import datetime, timezone
14
15
  from pathlib import Path
15
16
 
16
17
 
@@ -93,6 +94,85 @@ def _resolve_runtime_command(script_type: str) -> str:
93
94
  return NEXO_PYTHON
94
95
 
95
96
 
97
+ def _cron_run_db_path() -> Path:
98
+ return paths.data_dir() / "nexo.db"
99
+
100
+
101
+ def _utc_timestamp() -> str:
102
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
103
+
104
+
105
+ def _summarize_output(stdout: str, stderr: str) -> tuple[str, str]:
106
+ combined = "\n".join(part for part in (stdout or "", stderr or "") if part)
107
+ lines = [line.strip() for line in combined.splitlines() if line.strip()]
108
+ summary = (lines[-1] if lines else "")[:500]
109
+ error = ""
110
+ for line in reversed(lines):
111
+ lowered = line.lower()
112
+ if any(marker in lowered for marker in ("error", "exception", "fail", "traceback")):
113
+ error = line[:500]
114
+ break
115
+ return summary, error
116
+
117
+
118
+ def _start_direct_cron_run(cron_id: str) -> dict | None:
119
+ """Record a catch-up run when the shell wrapper is unavailable.
120
+
121
+ Normal installs use nexo-cron-wrapper.sh as the single writer. This
122
+ fallback exists for legacy/partially-migrated runtimes where catch-up can
123
+ still execute a script directly; without it, the run only updates
124
+ .catchup-state.json and stays invisible to cron health.
125
+ """
126
+ db_path = _cron_run_db_path()
127
+ started_at = _utc_timestamp()
128
+ try:
129
+ conn = sqlite3.connect(str(db_path))
130
+ try:
131
+ exists = conn.execute(
132
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='cron_runs'"
133
+ ).fetchone()
134
+ if not exists:
135
+ return None
136
+ cur = conn.execute(
137
+ "INSERT INTO cron_runs (cron_id, started_at, ended_at) VALUES (?, ?, NULL)",
138
+ (cron_id, started_at),
139
+ )
140
+ conn.commit()
141
+ return {"id": cur.lastrowid, "started_at": started_at}
142
+ finally:
143
+ conn.close()
144
+ except Exception as e:
145
+ log(f" WARNING: could not start direct cron_runs record for {cron_id}: {e}")
146
+ return None
147
+
148
+
149
+ def _finish_direct_cron_run(record: dict | None, cron_id: str, exit_code: int, stdout: str = "", stderr: str = ""):
150
+ if not record:
151
+ return
152
+ ended_at = _utc_timestamp()
153
+ summary, error = _summarize_output(stdout, stderr)
154
+ if exit_code != 0 and not error:
155
+ error = (stderr or stdout or f"exit {exit_code}")[:500]
156
+ db_path = _cron_run_db_path()
157
+ try:
158
+ conn = sqlite3.connect(str(db_path))
159
+ try:
160
+ conn.execute(
161
+ """
162
+ UPDATE cron_runs
163
+ SET ended_at=?, exit_code=?, summary=?, error=?,
164
+ duration_secs=(julianday(?) - julianday(started_at)) * 86400.0
165
+ WHERE id=?
166
+ """,
167
+ (ended_at, int(exit_code), summary, error, ended_at, int(record["id"])),
168
+ )
169
+ conn.commit()
170
+ finally:
171
+ conn.close()
172
+ except Exception as e:
173
+ log(f" WARNING: could not finish direct cron_runs record for {cron_id}: {e}")
174
+
175
+
96
176
  def _acquire_lock():
97
177
  LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
98
178
  handle = LOCK_FILE.open("w")
@@ -150,12 +230,14 @@ def run_task(candidate: dict, state: dict) -> bool:
150
230
  command = [runtime_cmd, str(script_path)]
151
231
 
152
232
  log(f" RUNNING {name}: {script_name}")
233
+ direct_record = None if WRAPPER.exists() else _start_direct_cron_run(name)
153
234
  try:
154
235
  result = subprocess.run(
155
236
  command,
156
237
  capture_output=True, text=True, timeout=AUTOMATION_SUBPROCESS_TIMEOUT,
157
238
  env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
158
239
  )
240
+ _finish_direct_cron_run(direct_record, name, result.returncode, result.stdout, result.stderr)
159
241
  if result.returncode == 0:
160
242
  log(f" OK {name} (exit 0)")
161
243
  state[name] = datetime.now().isoformat()
@@ -167,9 +249,11 @@ def run_task(candidate: dict, state: dict) -> bool:
167
249
  log(f" stderr: {result.stderr[:300]}")
168
250
  return False
169
251
  except subprocess.TimeoutExpired:
252
+ _finish_direct_cron_run(direct_record, name, 124, stderr=f"TIMEOUT after {AUTOMATION_SUBPROCESS_TIMEOUT}s")
170
253
  log(f" TIMEOUT {name} ({AUTOMATION_SUBPROCESS_TIMEOUT}s)")
171
254
  return False
172
255
  except Exception as e:
256
+ _finish_direct_cron_run(direct_record, name, 1, stderr=str(e))
173
257
  log(f" ERROR {name}: {e}")
174
258
  return False
175
259
 
@@ -28,6 +28,54 @@ LOG="$NEXO_HOME/runtime/logs/tcc-auto-approve.log"
28
28
 
29
29
  mkdir -p "$MARKER_DIR" "$(dirname "$LOG")"
30
30
 
31
+ FAILED=0
32
+ APPROVED_VERSIONS=0
33
+ PYTHON_APPROVED=0
34
+
35
+ log_line() {
36
+ echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG"
37
+ }
38
+
39
+ approve_service() {
40
+ local svc="$1"
41
+ local client="$2"
42
+ local output
43
+
44
+ if output=$(sqlite3 "$TCC_DB" "
45
+ INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
46
+ VALUES ('$svc', '$client', 1, 2, 4, 1);
47
+ " 2>&1); then
48
+ return 0
49
+ fi
50
+
51
+ log_line "WARN: failed TCC approval service=$svc client=$client: ${output:-sqlite3 failed}"
52
+ return 1
53
+ }
54
+
55
+ approve_client() {
56
+ local label="$1"
57
+ local client="$2"
58
+ local marker="${3:-}"
59
+ local failed=0
60
+
61
+ log_line "Approving $label"
62
+
63
+ for svc in "${SERVICES[@]}"; do
64
+ if ! approve_service "$svc" "$client"; then
65
+ failed=$((failed + 1))
66
+ fi
67
+ done
68
+
69
+ if [ "$failed" -eq 0 ]; then
70
+ [ -n "$marker" ] && touch "$marker"
71
+ log_line "Done: $label — ${#SERVICES[@]} services approved"
72
+ return 0
73
+ fi
74
+
75
+ log_line "FAILED: $label — $failed/${#SERVICES[@]} services failed"
76
+ return 1
77
+ }
78
+
31
79
  # TCC services Claude Code needs
32
80
  SERVICES=(
33
81
  kTCCServiceSystemPolicyDocumentsFolder
@@ -49,17 +97,11 @@ if [ -d "$VERSIONS_DIR" ]; then
49
97
  # Skip if already approved
50
98
  [ -f "$marker" ] && continue
51
99
 
52
- echo "$(date '+%Y-%m-%d %H:%M:%S') Approving Claude $version" >> "$LOG"
53
-
54
- for svc in "${SERVICES[@]}"; do
55
- sqlite3 "$TCC_DB" "
56
- INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
57
- VALUES ('$svc', '$bin_path', 1, 2, 4, 1);
58
- " 2>/dev/null
59
- done
60
-
61
- touch "$marker"
62
- echo "$(date '+%Y-%m-%d %H:%M:%S') Done: Claude $version — ${#SERVICES[@]} services approved" >> "$LOG"
100
+ if approve_client "Claude $version" "$bin_path" "$marker"; then
101
+ APPROVED_VERSIONS=$((APPROVED_VERSIONS + 1))
102
+ else
103
+ FAILED=1
104
+ fi
63
105
  done
64
106
  fi
65
107
 
@@ -69,11 +111,21 @@ if [ -n "$NEXO_CODE" ]; then
69
111
  PYTHON_BIN="$(dirname "$NEXO_CODE")/.venv/bin/python"
70
112
  if [ -e "$PYTHON_BIN" ]; then
71
113
  PYTHON_REAL=$(readlink -f "$PYTHON_BIN" 2>/dev/null || echo "$PYTHON_BIN")
72
- for svc in "${SERVICES[@]}"; do
73
- sqlite3 "$TCC_DB" "
74
- INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
75
- VALUES ('$svc', '$PYTHON_REAL', 1, 2, 4, 1);
76
- " 2>/dev/null
77
- done
114
+ if approve_client "NEXO Python" "$PYTHON_REAL"; then
115
+ PYTHON_APPROVED=1
116
+ else
117
+ FAILED=1
118
+ fi
78
119
  fi
79
120
  fi
121
+
122
+ if [ "$FAILED" -ne 0 ]; then
123
+ echo "TCC auto-approve failed; see $LOG" >&2
124
+ exit 1
125
+ fi
126
+
127
+ if [ "$APPROVED_VERSIONS" -eq 0 ] && [ "$PYTHON_APPROVED" -eq 0 ]; then
128
+ echo "TCC auto-approve: nothing pending"
129
+ else
130
+ echo "TCC auto-approve: approved $APPROVED_VERSIONS Claude version(s)"
131
+ fi