nexo-brain 7.20.18 → 7.20.20
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 +3 -1
- package/package.json +1 -1
- package/src/cli.py +190 -0
- package/src/crons/manifest.json +1 -1
- package/src/runtime_power.py +8 -0
- package/src/scripts/nexo-backup.sh +16 -3
- package/src/scripts/nexo-local-index.py +2 -1
- package/src/server.py +63 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.20.
|
|
3
|
+
"version": "7.20.19",
|
|
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.20.
|
|
21
|
+
Version `7.20.19` is the current packaged-runtime line. Patch release over v7.20.18 — Local Memory status and long first-indexing runs stay stable during Desktop-managed updates; stale macOS Full Disk Access denials are cleared after a live access probe succeeds.
|
|
22
|
+
|
|
23
|
+
Previously in `7.20.18`: patch release over v7.20.17 — Desktop-managed setup now preserves a completed onboarding flag when Brain is later invoked with the non-interactive `--skip` bootstrap path.
|
|
22
24
|
|
|
23
25
|
Previously in `7.20.17`: patch release over v7.20.16 — validated DB backups now tolerate tiny live-write growth from the Local Memory indexer while still rejecting real protected-table loss.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.20.
|
|
3
|
+
"version": "7.20.20",
|
|
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
|
@@ -140,6 +140,166 @@ def _mcp_status(args) -> int:
|
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def _mcp_write_message(stdin, payload: dict) -> None:
|
|
144
|
+
raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
145
|
+
stdin.write(raw + b"\n")
|
|
146
|
+
stdin.flush()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _mcp_reader(stdout, queue) -> None:
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
line = stdout.readline()
|
|
153
|
+
if not line:
|
|
154
|
+
return
|
|
155
|
+
text = line.decode("utf-8", errors="replace").strip()
|
|
156
|
+
if not text:
|
|
157
|
+
continue
|
|
158
|
+
queue.put(json.loads(text))
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
queue.put({"_reader_error": str(exc)})
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _stderr_reader(stderr, lines: list[str]) -> None:
|
|
165
|
+
while True:
|
|
166
|
+
chunk = stderr.readline()
|
|
167
|
+
if not chunk:
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
lines.append(chunk.decode("utf-8", errors="replace").rstrip())
|
|
171
|
+
except Exception:
|
|
172
|
+
lines.append(repr(chunk))
|
|
173
|
+
del lines[:-80]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _mcp_wait_for_response(queue, request_id: int, timeout_seconds: float) -> dict:
|
|
177
|
+
import queue as queue_module
|
|
178
|
+
|
|
179
|
+
deadline = time.monotonic() + max(timeout_seconds, 0.1)
|
|
180
|
+
while time.monotonic() < deadline:
|
|
181
|
+
remaining = max(deadline - time.monotonic(), 0.05)
|
|
182
|
+
try:
|
|
183
|
+
message = queue.get(timeout=remaining)
|
|
184
|
+
except queue_module.Empty:
|
|
185
|
+
break
|
|
186
|
+
if message.get("_reader_error"):
|
|
187
|
+
raise RuntimeError(message["_reader_error"])
|
|
188
|
+
if message.get("id") == request_id:
|
|
189
|
+
return message
|
|
190
|
+
raise TimeoutError(f"MCP response {request_id} timed out")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _mcp_probe(args) -> int:
|
|
194
|
+
import queue as queue_module
|
|
195
|
+
import threading
|
|
196
|
+
|
|
197
|
+
timeout_ms = int(getattr(args, "timeout_ms", 8000) or 8000)
|
|
198
|
+
timeout_seconds = max(timeout_ms / 1000.0, 1.0)
|
|
199
|
+
started_at = time.monotonic()
|
|
200
|
+
server_path = NEXO_CODE / "server.py"
|
|
201
|
+
env = os.environ.copy()
|
|
202
|
+
env["NEXO_MCP_PROBE"] = "1"
|
|
203
|
+
env.setdefault("NEXO_MCP_PLUGIN_MODE", getattr(args, "plugin_mode", None) or "none")
|
|
204
|
+
env.setdefault("NEXO_MCP_RUN_STARTUP_PREFLIGHT", "0")
|
|
205
|
+
client = str(getattr(args, "client", "") or "").strip()
|
|
206
|
+
if client:
|
|
207
|
+
env["NEXO_MCP_CLIENT"] = client
|
|
208
|
+
|
|
209
|
+
proc = None
|
|
210
|
+
stderr_lines: list[str] = []
|
|
211
|
+
try:
|
|
212
|
+
proc = subprocess.Popen(
|
|
213
|
+
[sys.executable, str(server_path)],
|
|
214
|
+
cwd=str(NEXO_CODE),
|
|
215
|
+
env=env,
|
|
216
|
+
stdin=subprocess.PIPE,
|
|
217
|
+
stdout=subprocess.PIPE,
|
|
218
|
+
stderr=subprocess.PIPE,
|
|
219
|
+
text=False,
|
|
220
|
+
)
|
|
221
|
+
responses = queue_module.Queue()
|
|
222
|
+
threading.Thread(target=_mcp_reader, args=(proc.stdout, responses), daemon=True).start()
|
|
223
|
+
threading.Thread(target=_stderr_reader, args=(proc.stderr, stderr_lines), daemon=True).start()
|
|
224
|
+
|
|
225
|
+
_mcp_write_message(proc.stdin, {
|
|
226
|
+
"jsonrpc": "2.0",
|
|
227
|
+
"id": 1,
|
|
228
|
+
"method": "initialize",
|
|
229
|
+
"params": {
|
|
230
|
+
"protocolVersion": "2024-11-05",
|
|
231
|
+
"capabilities": {},
|
|
232
|
+
"clientInfo": {"name": "nexo-mcp-probe", "version": _get_version()},
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
init_response = _mcp_wait_for_response(responses, 1, timeout_seconds)
|
|
236
|
+
if init_response.get("error"):
|
|
237
|
+
raise RuntimeError(f"MCP initialize failed: {init_response['error']}")
|
|
238
|
+
|
|
239
|
+
_mcp_write_message(proc.stdin, {
|
|
240
|
+
"jsonrpc": "2.0",
|
|
241
|
+
"method": "notifications/initialized",
|
|
242
|
+
"params": {},
|
|
243
|
+
})
|
|
244
|
+
_mcp_write_message(proc.stdin, {
|
|
245
|
+
"jsonrpc": "2.0",
|
|
246
|
+
"id": 2,
|
|
247
|
+
"method": "tools/list",
|
|
248
|
+
"params": {},
|
|
249
|
+
})
|
|
250
|
+
tools_response = _mcp_wait_for_response(responses, 2, timeout_seconds)
|
|
251
|
+
if tools_response.get("error"):
|
|
252
|
+
raise RuntimeError(f"MCP tools/list failed: {tools_response['error']}")
|
|
253
|
+
tools = ((tools_response.get("result") or {}).get("tools") or [])
|
|
254
|
+
tool_names = [
|
|
255
|
+
str(tool.get("name") or "")
|
|
256
|
+
for tool in tools
|
|
257
|
+
if isinstance(tool, dict) and tool.get("name")
|
|
258
|
+
]
|
|
259
|
+
required = ["nexo_startup", "nexo_heartbeat", "nexo_task_open", "nexo_guard_check"]
|
|
260
|
+
missing = [name for name in required if name not in tool_names]
|
|
261
|
+
ok = not missing and len(tool_names) > 0
|
|
262
|
+
payload = {
|
|
263
|
+
"ok": ok,
|
|
264
|
+
"mcp_ready": ok,
|
|
265
|
+
"probe_ok": ok,
|
|
266
|
+
"tools_available": len(tool_names) > 0,
|
|
267
|
+
"tool_count": len(tool_names),
|
|
268
|
+
"required_tools_present": not missing,
|
|
269
|
+
"missing_required_tools": missing,
|
|
270
|
+
"client": client,
|
|
271
|
+
"plugin_mode": env.get("NEXO_MCP_PLUGIN_MODE"),
|
|
272
|
+
"elapsed_ms": int((time.monotonic() - started_at) * 1000),
|
|
273
|
+
"stderr_tail": "\n".join(stderr_lines[-12:]),
|
|
274
|
+
}
|
|
275
|
+
return _print_json_or_text(payload, as_json=bool(getattr(args, "json", False)))
|
|
276
|
+
except Exception as exc:
|
|
277
|
+
payload = {
|
|
278
|
+
"ok": False,
|
|
279
|
+
"mcp_ready": False,
|
|
280
|
+
"probe_ok": False,
|
|
281
|
+
"error": "mcp_probe_failed",
|
|
282
|
+
"message": str(exc),
|
|
283
|
+
"client": client,
|
|
284
|
+
"elapsed_ms": int((time.monotonic() - started_at) * 1000),
|
|
285
|
+
"stderr_tail": "\n".join(stderr_lines[-20:]),
|
|
286
|
+
}
|
|
287
|
+
return _print_json_or_text(payload, as_json=bool(getattr(args, "json", False)))
|
|
288
|
+
finally:
|
|
289
|
+
if proc is not None:
|
|
290
|
+
try:
|
|
291
|
+
proc.terminate()
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
try:
|
|
295
|
+
proc.wait(timeout=2)
|
|
296
|
+
except Exception:
|
|
297
|
+
try:
|
|
298
|
+
proc.kill()
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
|
|
143
303
|
def _mcp_clear_restart(args) -> int:
|
|
144
304
|
return _print_json_or_text(
|
|
145
305
|
clear_restart_required_marker(
|
|
@@ -1389,6 +1549,17 @@ def _local_context_models(args) -> int:
|
|
|
1389
1549
|
return _local_context_emit(local_context.model_status(), args)
|
|
1390
1550
|
|
|
1391
1551
|
|
|
1552
|
+
def _local_context_performance(args) -> int:
|
|
1553
|
+
import local_context
|
|
1554
|
+
command = str(getattr(args, "local_context_performance_command", "") or "status")
|
|
1555
|
+
if command == "set":
|
|
1556
|
+
return _local_context_emit(
|
|
1557
|
+
local_context.set_performance_profile(getattr(args, "profile", "") or "medium"),
|
|
1558
|
+
args,
|
|
1559
|
+
)
|
|
1560
|
+
return _local_context_emit({"ok": True, "performance": local_context.performance_config()}, args)
|
|
1561
|
+
|
|
1562
|
+
|
|
1392
1563
|
def _local_context_service_config(args) -> int:
|
|
1393
1564
|
from local_context import api as local_context_api
|
|
1394
1565
|
return _local_context_emit(
|
|
@@ -3238,6 +3409,14 @@ def main():
|
|
|
3238
3409
|
local_context_models_warmup_p.add_argument("--allow-download", action="store_true", help="Allow model downloads")
|
|
3239
3410
|
local_context_models_warmup_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3240
3411
|
|
|
3412
|
+
local_context_performance_p = local_context_sub.add_parser("performance", help="Show or change local memory indexing speed")
|
|
3413
|
+
local_context_performance_sub = local_context_performance_p.add_subparsers(dest="local_context_performance_command")
|
|
3414
|
+
local_context_performance_status_p = local_context_performance_sub.add_parser("status", help="Show active indexing speed")
|
|
3415
|
+
local_context_performance_status_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3416
|
+
local_context_performance_set_p = local_context_performance_sub.add_parser("set", help="Set indexing speed")
|
|
3417
|
+
local_context_performance_set_p.add_argument("profile", choices=["low", "medium", "high", "extreme"], help="Indexing speed profile")
|
|
3418
|
+
local_context_performance_set_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3419
|
+
|
|
3241
3420
|
local_context_asset_p = local_context_sub.add_parser("asset", help="Inspect or purge one indexed asset")
|
|
3242
3421
|
local_context_asset_sub = local_context_asset_p.add_subparsers(dest="local_context_asset_command")
|
|
3243
3422
|
local_context_asset_get_p = local_context_asset_sub.add_parser("get", help="Get indexed asset details")
|
|
@@ -3543,6 +3722,11 @@ def main():
|
|
|
3543
3722
|
mcp_status_p = mcp_sub.add_parser("status", help="Read the current runtime/MCP alignment state")
|
|
3544
3723
|
mcp_status_p.add_argument("--client", default="", help="Optional client label such as claude_desktop or codex")
|
|
3545
3724
|
mcp_status_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3725
|
+
mcp_probe_p = mcp_sub.add_parser("probe", help="Launch the MCP server and verify initialize + tools/list")
|
|
3726
|
+
mcp_probe_p.add_argument("--client", default="", help="Optional client label such as claude_desktop or codex")
|
|
3727
|
+
mcp_probe_p.add_argument("--timeout-ms", type=int, default=8000)
|
|
3728
|
+
mcp_probe_p.add_argument("--plugin-mode", default="none", choices=["essential", "none", "full"])
|
|
3729
|
+
mcp_probe_p.add_argument("--json", action="store_true", help="JSON output")
|
|
3546
3730
|
mcp_clear_p = mcp_sub.add_parser("clear-restart", help="Acknowledge that a client/session reloaded the new runtime")
|
|
3547
3731
|
mcp_clear_p.add_argument("--client", default="", help="Client label such as claude_desktop or codex")
|
|
3548
3732
|
mcp_clear_p.add_argument("--installed-version", default="")
|
|
@@ -3784,6 +3968,10 @@ def main():
|
|
|
3784
3968
|
if not args.local_context_models_command:
|
|
3785
3969
|
args.local_context_models_command = "status"
|
|
3786
3970
|
return _local_context_models(args)
|
|
3971
|
+
if args.local_context_command == "performance":
|
|
3972
|
+
if not args.local_context_performance_command:
|
|
3973
|
+
args.local_context_performance_command = "status"
|
|
3974
|
+
return _local_context_performance(args)
|
|
3787
3975
|
if args.local_context_command == "asset":
|
|
3788
3976
|
if not args.local_context_asset_command:
|
|
3789
3977
|
local_context_asset_p.print_help()
|
|
@@ -3842,6 +4030,8 @@ def main():
|
|
|
3842
4030
|
elif args.command == "mcp":
|
|
3843
4031
|
if args.mcp_command == "status":
|
|
3844
4032
|
return _mcp_status(args)
|
|
4033
|
+
if args.mcp_command == "probe":
|
|
4034
|
+
return _mcp_probe(args)
|
|
3845
4035
|
if args.mcp_command == "clear-restart":
|
|
3846
4036
|
return _mcp_clear_restart(args)
|
|
3847
4037
|
mcp_parser.print_help()
|
package/src/crons/manifest.json
CHANGED
package/src/runtime_power.py
CHANGED
|
@@ -566,6 +566,14 @@ def detect_full_disk_access_reasons(*, system: str | None = None) -> list[str]:
|
|
|
566
566
|
if system != "Darwin":
|
|
567
567
|
return []
|
|
568
568
|
|
|
569
|
+
try:
|
|
570
|
+
probe = probe_full_disk_access()
|
|
571
|
+
if probe.get("granted") is True:
|
|
572
|
+
clear_full_disk_access_required_state()
|
|
573
|
+
return []
|
|
574
|
+
except Exception:
|
|
575
|
+
pass
|
|
576
|
+
|
|
569
577
|
reasons: list[str] = []
|
|
570
578
|
if _is_protected_macos_path(NEXO_HOME):
|
|
571
579
|
reasons.append(
|
|
@@ -14,13 +14,26 @@ mkdir -p "$BACKUP_DIR" "$WEEKLY_DIR"
|
|
|
14
14
|
|
|
15
15
|
# Hourly backup
|
|
16
16
|
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
|
|
17
|
-
|
|
17
|
+
BACKUP_FILE="$BACKUP_DIR/nexo-$TIMESTAMP.db"
|
|
18
|
+
TMP_BACKUP="$BACKUP_FILE.tmp.$$"
|
|
19
|
+
rm -f "$TMP_BACKUP"
|
|
20
|
+
if sqlite3 -cmd ".timeout 60000" "$DB" <<SQL
|
|
21
|
+
PRAGMA busy_timeout=60000;
|
|
22
|
+
.backup '$TMP_BACKUP'
|
|
23
|
+
SQL
|
|
24
|
+
then
|
|
25
|
+
mv "$TMP_BACKUP" "$BACKUP_FILE"
|
|
26
|
+
else
|
|
27
|
+
rm -f "$TMP_BACKUP"
|
|
28
|
+
echo "NEXO backup failed: database busy or unavailable" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
18
31
|
|
|
19
32
|
# Weekly backup — save one per week (Sundays)
|
|
20
33
|
WEEK=$(date +%Y-W%V)
|
|
21
34
|
WEEKLY_FILE="$WEEKLY_DIR/weekly-$WEEK.db"
|
|
22
|
-
if [ ! -f "$WEEKLY_FILE" ] && [ "$(date +%u)" = "7" ]; then
|
|
23
|
-
cp "$
|
|
35
|
+
if [ ! -f "$WEEKLY_FILE" ] && [ "$(date +%u)" = "7" ] && [ -f "$BACKUP_FILE" ]; then
|
|
36
|
+
cp "$BACKUP_FILE" "$WEEKLY_FILE"
|
|
24
37
|
fi
|
|
25
38
|
|
|
26
39
|
# Cleanup: hourly >48h, weekly >90 days
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# nexo: description=Cooperative local memory indexing cycle for Brain/Desktop.
|
|
4
4
|
# nexo: category=memory
|
|
5
5
|
# nexo: runtime=python
|
|
6
|
-
# nexo: timeout=
|
|
6
|
+
# nexo: timeout=21600
|
|
7
7
|
# nexo: cron_id=local-index
|
|
8
8
|
# nexo: interval_seconds=60
|
|
9
9
|
# nexo: schedule_required=true
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# nexo: run_on_wake=true
|
|
13
13
|
# nexo: idempotent=true
|
|
14
14
|
# nexo: max_catchup_age=600
|
|
15
|
+
# nexo: stuck_after_seconds=21600
|
|
15
16
|
# nexo: doctor_allow_db=true
|
|
16
17
|
|
|
17
18
|
from __future__ import annotations
|
package/src/server.py
CHANGED
|
@@ -259,8 +259,56 @@ def _run_startup_preflight_sync() -> None:
|
|
|
259
259
|
print(f"[NEXO auto-update] error: {e}", file=sys.stderr)
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
_ESSENTIAL_MCP_STARTUP_PLUGINS = (
|
|
263
|
+
"cards.py",
|
|
264
|
+
"doctor.py",
|
|
265
|
+
"episodic_memory.py",
|
|
266
|
+
"evolution.py",
|
|
267
|
+
"lifecycle_events.py",
|
|
268
|
+
"outcomes.py",
|
|
269
|
+
"preferences.py",
|
|
270
|
+
"protocol.py",
|
|
271
|
+
"recover.py",
|
|
272
|
+
"skills.py",
|
|
273
|
+
"user_state_tools.py",
|
|
274
|
+
"workflow.py",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _env_flag(name: str, *, default: bool = False) -> bool:
|
|
279
|
+
value = os.environ.get(name)
|
|
280
|
+
if value is None:
|
|
281
|
+
return default
|
|
282
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on", "y", "si"}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _mcp_startup_plugin_mode() -> str:
|
|
286
|
+
return str(os.environ.get("NEXO_MCP_PLUGIN_MODE", "none") or "none").strip().lower()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _load_startup_plugins() -> None:
|
|
290
|
+
mode = _mcp_startup_plugin_mode()
|
|
291
|
+
if mode in {"none", "off", "0", "false"}:
|
|
292
|
+
print("[NEXO] MCP dynamic plugin loading skipped.", file=sys.stderr)
|
|
293
|
+
return
|
|
294
|
+
if mode in {"full", "all", "legacy"}:
|
|
295
|
+
load_all_plugins(mcp)
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
if mode not in {"essential", "fast", "default"}:
|
|
299
|
+
print(f"[NEXO] Unknown NEXO_MCP_PLUGIN_MODE={mode!r}; using essential plugins.", file=sys.stderr)
|
|
300
|
+
|
|
301
|
+
loaded = 0
|
|
302
|
+
for filename in _ESSENTIAL_MCP_STARTUP_PLUGINS:
|
|
303
|
+
try:
|
|
304
|
+
loaded += int(load_plugin(mcp, filename) or 0)
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
print(f"[PLUGIN ERROR] {filename}: {exc}", file=sys.stderr)
|
|
307
|
+
print(f"[NEXO] MCP essential plugins ready: {loaded} tools.", file=sys.stderr)
|
|
308
|
+
|
|
309
|
+
|
|
262
310
|
def _server_init():
|
|
263
|
-
"""Run
|
|
311
|
+
"""Run side effects needed by the MCP server.
|
|
264
312
|
|
|
265
313
|
Called only when the server is actually started (not on import).
|
|
266
314
|
"""
|
|
@@ -268,20 +316,26 @@ def _server_init():
|
|
|
268
316
|
signal.signal(signal.SIGINT, _shutdown_handler)
|
|
269
317
|
|
|
270
318
|
# ── Write PID file for stale process detection ─────────────────
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
319
|
+
if not _env_flag("NEXO_MCP_PROBE"):
|
|
320
|
+
data_dir = _data_dir()
|
|
321
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
322
|
+
_pid_file = os.path.join(data_dir, "nexo.pid")
|
|
323
|
+
with open(_pid_file, "w") as f:
|
|
324
|
+
f.write(str(os.getpid()))
|
|
276
325
|
|
|
277
326
|
# ── Database initialization with recovery ─────────────────────
|
|
278
327
|
_init_db_or_exit()
|
|
279
328
|
|
|
280
|
-
# ── Auto-update / startup preflight
|
|
281
|
-
|
|
329
|
+
# ── Auto-update / startup preflight ───────────────────────────
|
|
330
|
+
# The MCP client waits for an immediate JSON-RPC handshake. Running update
|
|
331
|
+
# checks here can block the transport and make clients start without NEXO.
|
|
332
|
+
if _env_flag("NEXO_MCP_RUN_STARTUP_PREFLIGHT"):
|
|
333
|
+
_run_startup_preflight_sync()
|
|
334
|
+
else:
|
|
335
|
+
print("[NEXO] MCP startup preflight deferred.", file=sys.stderr)
|
|
282
336
|
|
|
283
337
|
# ── Load plugins ───────────────────────────────────────────────
|
|
284
|
-
|
|
338
|
+
_load_startup_plugins()
|
|
285
339
|
|
|
286
340
|
|
|
287
341
|
mcp = FastMCP(
|