nexo-brain 7.17.5 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/scripts/nexo-catchup.py +85 -1
- package/src/scripts/nexo-tcc-approve.sh +69 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.17.
|
|
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,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.17.
|
|
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
22
|
|
|
23
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.
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.17.
|
|
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",
|
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|