nexo-brain 7.9.5 → 7.9.7

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.9.4",
3
+ "version": "7.9.7",
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.9.5` is the current packaged-runtime line. Patch release that fixes canonical diary confirmation for Desktop: Brain now resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.4 shutdown/archive/delete checks.
21
+ Version `7.9.7` is the current packaged-runtime line. Patch release that hardens the installed-user MCP/update path after live Desktop validation: Brain now normalizes managed runtime configs back to the stable `~/.nexo/core` root instead of leaving clients pinned to versioned snapshots, mirrors Claude Code MCP config into `~/.claude/mcp-cortex.json`, stamps the active MCP client in generated configs, and disables the FastMCP wrapped `output_schema` that was making Claude Code reject plain-text `nexo_*` tool results. Coordinated Desktop v0.28.8 consumes the same contract so archive/reopen/app-quit flows recover cleanly without reviving stale sessions.
22
+
23
+ Previously in `7.9.5`: patch release that fixes canonical diary confirmation for Desktop: Brain resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.6 shutdown/archive/delete/app-exit checks.
22
24
 
23
25
  Previously in `7.9.4`: patch release that blocks the Brain 7.9.3 + Desktop 0.28.2 diary regression: canonical lifecycle plans now require real `session_diary` evidence (`wait_for_diary_write`) before `stop_session`, and canonical completion is rejected/retryable without that diary row. It also fixes npm CLI onboarding so `nexo-brain --version` and subcommands never launch the wizard when legacy/v2 calibration is already valid, commits setup calibration atomically only after the wizard completes, and adds `nexo-brain warmup-models` so install/update paths predownload the local mDeBERTa/BGE/reranker models. Verification: full Brain pytest (`2189 passed, 3 skipped, 1 xfailed, 5 xpassed`), release-readiness, npm pack dry-run, and coordinated Desktop v0.28.3 checks.
24
26
 
package/bin/nexo-brain.js CHANGED
@@ -3249,6 +3249,10 @@ async function runSetup() {
3249
3249
  ' printf \'%s\\n\' "${NEXO_CODE%/}"',
3250
3250
  ' return 0',
3251
3251
  ' fi',
3252
+ ' if [ -f "$NEXO_HOME/core/current/cli.py" ]; then',
3253
+ ' printf \'%s\\n\' "$NEXO_HOME/core/current"',
3254
+ ' return 0',
3255
+ ' fi',
3252
3256
  ' if [ -f "$NEXO_HOME/core/cli.py" ]; then',
3253
3257
  ' printf \'%s\\n\' "$NEXO_HOME/core"',
3254
3258
  ' return 0',
@@ -3302,6 +3306,11 @@ async function runSetup() {
3302
3306
  ' exit 1',
3303
3307
  'fi',
3304
3308
  'CLI_PY="$NEXO_CODE/cli.py"',
3309
+ 'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/current/cli.py" ]; then',
3310
+ ' NEXO_CODE="$NEXO_HOME/core/current"',
3311
+ ' export NEXO_CODE',
3312
+ ' CLI_PY="$NEXO_HOME/core/current/cli.py"',
3313
+ 'fi',
3305
3314
  'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/cli.py" ]; then',
3306
3315
  ' NEXO_CODE="$NEXO_HOME/core"',
3307
3316
  ' export NEXO_CODE',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.5",
3
+ "version": "7.9.7",
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",
@@ -537,6 +537,10 @@ def _runtime_cli_wrapper_text() -> str:
537
537
  ' printf \'%s\\n\' "${NEXO_CODE%/}"\n'
538
538
  ' return 0\n'
539
539
  ' fi\n'
540
+ ' if [ -f "$NEXO_HOME/core/current/cli.py" ]; then\n'
541
+ ' printf \'%s\\n\' "$NEXO_HOME/core/current"\n'
542
+ ' return 0\n'
543
+ ' fi\n'
540
544
  ' if [ -f "$NEXO_HOME/core/cli.py" ]; then\n'
541
545
  ' printf \'%s\\n\' "$NEXO_HOME/core"\n'
542
546
  ' return 0\n'
@@ -590,6 +594,11 @@ def _runtime_cli_wrapper_text() -> str:
590
594
  ' exit 1\n'
591
595
  'fi\n'
592
596
  'CLI_PY="$NEXO_CODE/cli.py"\n'
597
+ 'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/current/cli.py" ]; then\n'
598
+ ' NEXO_CODE="$NEXO_HOME/core/current"\n'
599
+ ' export NEXO_CODE\n'
600
+ ' CLI_PY="$NEXO_HOME/core/current/cli.py"\n'
601
+ 'fi\n'
593
602
  'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/cli.py" ]; then\n'
594
603
  ' NEXO_CODE="$NEXO_HOME/core"\n'
595
604
  ' export NEXO_CODE\n'
@@ -2253,19 +2262,20 @@ def _maybe_backfill_task_owner() -> None:
2253
2262
 
2254
2263
 
2255
2264
  def _f06_legacy_shim_map() -> list[tuple[str, Path]]:
2265
+ core_root = NEXO_HOME / "core"
2256
2266
  return [
2257
- ("scripts", NEXO_HOME / "core" / "scripts"),
2258
- ("skills-core", paths.core_skills_dir(allow_legacy_fallback=False)),
2259
- ("db", paths.core_db_dir(allow_legacy_fallback=False)),
2260
- ("cognitive", paths.core_cognitive_dir(allow_legacy_fallback=False)),
2261
- ("doctor", paths.core_doctor_dir(allow_legacy_fallback=False)),
2262
- ("dashboard", paths.core_dashboard_dir(allow_legacy_fallback=False)),
2267
+ ("scripts", core_root / "scripts"),
2268
+ ("skills-core", core_root / "skills"),
2269
+ ("db", core_root / "db"),
2270
+ ("cognitive", core_root / "cognitive"),
2271
+ ("doctor", core_root / "doctor"),
2272
+ ("dashboard", core_root / "dashboard"),
2263
2273
  ("brain", NEXO_HOME / "personal" / "brain"),
2264
2274
  ("config", NEXO_HOME / "personal" / "config"),
2265
2275
  ("skills", NEXO_HOME / "personal" / "skills"),
2266
- ("plugins", NEXO_HOME / "core" / "plugins"),
2267
- ("hooks", NEXO_HOME / "core" / "hooks"),
2268
- ("rules", NEXO_HOME / "core" / "rules"),
2276
+ ("plugins", core_root / "plugins"),
2277
+ ("hooks", core_root / "hooks"),
2278
+ ("rules", core_root / "rules"),
2269
2279
  ("data", NEXO_HOME / "runtime" / "data"),
2270
2280
  ("logs", NEXO_HOME / "runtime" / "logs"),
2271
2281
  ("operations", NEXO_HOME / "runtime" / "operations"),
@@ -2291,7 +2301,7 @@ def _f06_legacy_file_shim_map() -> list[tuple[str, Path]]:
2291
2301
  def _f06_packaged_code_file_targets() -> list[tuple[str, Path]]:
2292
2302
  names = list(_discover_runtime_root_python_modules(NEXO_HOME))
2293
2303
  names.extend(_discover_runtime_root_python_modules(NEXO_HOME / "core"))
2294
- for extra in ("requirements.txt", "model_defaults.json"):
2304
+ for extra in ("requirements.txt", "model_defaults.json", "package.json", "version.json"):
2295
2305
  candidate = NEXO_HOME / extra
2296
2306
  canonical_candidate = NEXO_HOME / "core" / extra
2297
2307
  if candidate.exists() or canonical_candidate.exists():
@@ -2413,12 +2423,13 @@ def _promote_packaged_runtime_code_to_core() -> None:
2413
2423
  return resolved
2414
2424
  return path
2415
2425
 
2426
+ core_root = NEXO_HOME / "core"
2416
2427
  dir_targets = [
2417
- ("db", paths.core_db_dir(allow_legacy_fallback=False)),
2418
- ("cognitive", paths.core_cognitive_dir(allow_legacy_fallback=False)),
2419
- ("doctor", paths.core_doctor_dir(allow_legacy_fallback=False)),
2420
- ("dashboard", paths.core_dashboard_dir(allow_legacy_fallback=False)),
2421
- ("skills-core", paths.core_skills_dir(allow_legacy_fallback=False)),
2428
+ ("db", core_root / "db"),
2429
+ ("cognitive", core_root / "cognitive"),
2430
+ ("doctor", core_root / "doctor"),
2431
+ ("dashboard", core_root / "dashboard"),
2432
+ ("skills-core", core_root / "skills"),
2422
2433
  ]
2423
2434
 
2424
2435
  for legacy_name, canonical in dir_targets:
@@ -2895,11 +2906,11 @@ def _maybe_migrate_to_f06_layout() -> None:
2895
2906
  ("state", NEXO_HOME / "runtime" / "state"),
2896
2907
  ("backups", NEXO_HOME / "runtime" / "backups"),
2897
2908
  ("memory", NEXO_HOME / "runtime" / "memory"),
2898
- ("cognitive", paths.core_cognitive_dir(allow_legacy_fallback=False)),
2909
+ ("cognitive", NEXO_HOME / "core" / "cognitive"),
2899
2910
  ("coordination", NEXO_HOME / "runtime" / "coordination"),
2900
2911
  ("exports", NEXO_HOME / "runtime" / "exports"),
2901
2912
  ("nexo-email", NEXO_HOME / "runtime" / "nexo-email"),
2902
- ("doctor", paths.core_doctor_dir(allow_legacy_fallback=False)),
2913
+ ("doctor", NEXO_HOME / "core" / "doctor"),
2903
2914
  ("snapshots", NEXO_HOME / "runtime" / "snapshots"),
2904
2915
  ("crons", NEXO_HOME / "runtime" / "crons"),
2905
2916
  ("skills", NEXO_HOME / "personal" / "skills"),
@@ -3946,19 +3957,19 @@ def _resolve_sync_source() -> tuple[Path | None, Path | None]:
3946
3957
  dest = NEXO_HOME
3947
3958
 
3948
3959
  def _runtime_version_source() -> Path | None:
3949
- version_file = NEXO_HOME / "version.json"
3950
- if not version_file.is_file():
3951
- return None
3952
- try:
3953
- data = json.loads(version_file.read_text())
3954
- except Exception:
3955
- return None
3956
- source = str(data.get("source", "")).strip()
3957
- if not source:
3958
- return None
3959
- candidate = Path(source).expanduser()
3960
- if (candidate / "src").is_dir() and (candidate / "package.json").is_file():
3961
- return candidate
3960
+ for version_file in (NEXO_HOME / "version.json", NEXO_HOME / "core" / "version.json"):
3961
+ if not version_file.is_file():
3962
+ continue
3963
+ try:
3964
+ data = json.loads(version_file.read_text())
3965
+ except Exception:
3966
+ continue
3967
+ source = str(data.get("source", "")).strip()
3968
+ if not source:
3969
+ continue
3970
+ candidate = Path(source).expanduser()
3971
+ if (candidate / "src").is_dir() and (candidate / "package.json").is_file():
3972
+ return candidate
3962
3973
  return None
3963
3974
 
3964
3975
  try:
@@ -27,6 +27,14 @@ def _resolve_templates_dir(module_file: str | os.PathLike[str]) -> Path:
27
27
  parent = module_dir.parent / "templates"
28
28
  if parent.is_dir():
29
29
  return parent
30
+ try:
31
+ nexo_home_templates = resolve_nexo_home(
32
+ os.environ.get("NEXO_HOME", str(_user_home() / ".nexo"))
33
+ ) / "templates"
34
+ except Exception:
35
+ nexo_home_templates = Path(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo"))).expanduser() / "templates"
36
+ if nexo_home_templates.is_dir():
37
+ return nexo_home_templates
30
38
  return direct
31
39
 
32
40
 
package/src/cli.py CHANGED
@@ -47,6 +47,7 @@ import time
47
47
  from pathlib import Path
48
48
 
49
49
  from runtime_home import export_resolved_nexo_home
50
+ from runtime_versioning import build_mcp_status, clear_restart_required_marker
50
51
 
51
52
  NEXO_HOME = export_resolved_nexo_home()
52
53
  NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
@@ -120,6 +121,82 @@ def _fetch_latest_version(timeout_seconds: int = 2) -> str | None:
120
121
  return latest
121
122
 
122
123
 
124
+ def _print_json_or_text(payload: dict, *, as_json: bool = False) -> int:
125
+ if as_json:
126
+ print(json.dumps(payload, ensure_ascii=False))
127
+ else:
128
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
129
+ return 0 if payload.get("ok", True) else 1
130
+
131
+
132
+ def _mcp_status(args) -> int:
133
+ return _print_json_or_text(
134
+ build_mcp_status(client=getattr(args, "client", "") or ""),
135
+ as_json=bool(getattr(args, "json", False)),
136
+ )
137
+
138
+
139
+ def _mcp_clear_restart(args) -> int:
140
+ return _print_json_or_text(
141
+ clear_restart_required_marker(
142
+ client=getattr(args, "client", "") or "",
143
+ installed_version=getattr(args, "installed_version", "") or "",
144
+ process_version=getattr(args, "process_version", "") or "",
145
+ ),
146
+ as_json=bool(getattr(args, "json", False)),
147
+ )
148
+
149
+
150
+ def _continuity_snapshot_write(args) -> int:
151
+ from continuity import write_snapshot
152
+
153
+ result = write_snapshot(
154
+ conversation_id=args.conversation_id,
155
+ session_id=args.session_id or "",
156
+ external_session_id=args.external_session_id or "",
157
+ client=args.client or "",
158
+ event_type=args.event_type or "turn_end",
159
+ payload=args.payload or "",
160
+ trace_id=args.trace_id or "",
161
+ idempotency_key=args.idempotency_key or "",
162
+ )
163
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
164
+
165
+
166
+ def _continuity_snapshot_read(args) -> int:
167
+ from continuity import read_snapshot
168
+
169
+ result = read_snapshot(
170
+ conversation_id=args.conversation_id or "",
171
+ session_id=args.session_id or "",
172
+ limit=args.limit or 20,
173
+ )
174
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
175
+
176
+
177
+ def _continuity_resume_bundle(args) -> int:
178
+ from continuity import build_resume_bundle
179
+
180
+ result = build_resume_bundle(
181
+ conversation_id=args.conversation_id or "",
182
+ session_id=args.session_id or "",
183
+ external_session_id=args.external_session_id or "",
184
+ client=args.client or "",
185
+ token_budget=args.token_budget or 2000,
186
+ )
187
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
188
+
189
+
190
+ def _continuity_audit(args) -> int:
191
+ from continuity import continuity_audit
192
+
193
+ result = continuity_audit(
194
+ conversation_id=args.conversation_id,
195
+ limit=args.limit or 50,
196
+ )
197
+ return _print_json_or_text(result, as_json=bool(getattr(args, "json", False)))
198
+
199
+
123
200
  def _should_refresh_latest_version() -> bool:
124
201
  """Decide whether to hit the npm registry to refresh `latest` version.
125
202
 
@@ -3075,6 +3152,49 @@ def main():
3075
3152
  dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
3076
3153
  dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
3077
3154
 
3155
+ mcp_parser = sub.add_parser("mcp", help="MCP runtime status and restart state")
3156
+ mcp_sub = mcp_parser.add_subparsers(dest="mcp_command")
3157
+ mcp_status_p = mcp_sub.add_parser("status", help="Read the current runtime/MCP alignment state")
3158
+ mcp_status_p.add_argument("--client", default="", help="Optional client label such as claude_desktop or codex")
3159
+ mcp_status_p.add_argument("--json", action="store_true", help="JSON output")
3160
+ mcp_clear_p = mcp_sub.add_parser("clear-restart", help="Acknowledge that a client/session reloaded the new runtime")
3161
+ mcp_clear_p.add_argument("--client", default="", help="Client label such as claude_desktop or codex")
3162
+ mcp_clear_p.add_argument("--installed-version", default="")
3163
+ mcp_clear_p.add_argument("--process-version", default="")
3164
+ mcp_clear_p.add_argument("--json", action="store_true", help="JSON output")
3165
+
3166
+ continuity_parser = sub.add_parser("continuity", help="Continuity snapshots and resume bundles")
3167
+ continuity_sub = continuity_parser.add_subparsers(dest="continuity_command")
3168
+ cwrite_p = continuity_sub.add_parser("snapshot-write", help="Write a continuity snapshot")
3169
+ cwrite_p.add_argument("--conversation-id", required=True)
3170
+ cwrite_p.add_argument("--session-id", default="")
3171
+ cwrite_p.add_argument("--external-session-id", default="")
3172
+ cwrite_p.add_argument("--client", default="")
3173
+ cwrite_p.add_argument("--event-type", default="turn_end")
3174
+ cwrite_p.add_argument("--payload", default="")
3175
+ cwrite_p.add_argument("--trace-id", default="")
3176
+ cwrite_p.add_argument("--idempotency-key", default="")
3177
+ cwrite_p.add_argument("--json", action="store_true", help="JSON output")
3178
+
3179
+ cread_p = continuity_sub.add_parser("snapshot-read", help="Read continuity snapshots")
3180
+ cread_p.add_argument("--conversation-id", default="")
3181
+ cread_p.add_argument("--session-id", default="")
3182
+ cread_p.add_argument("--limit", type=int, default=20)
3183
+ cread_p.add_argument("--json", action="store_true", help="JSON output")
3184
+
3185
+ cbundle_p = continuity_sub.add_parser("resume-bundle", help="Build a continuity resume bundle")
3186
+ cbundle_p.add_argument("--conversation-id", default="")
3187
+ cbundle_p.add_argument("--session-id", default="")
3188
+ cbundle_p.add_argument("--external-session-id", default="")
3189
+ cbundle_p.add_argument("--client", default="")
3190
+ cbundle_p.add_argument("--token-budget", type=int, default=2000)
3191
+ cbundle_p.add_argument("--json", action="store_true", help="JSON output")
3192
+
3193
+ caudit_p = continuity_sub.add_parser("audit", help="Read the continuity audit timeline")
3194
+ caudit_p.add_argument("--conversation-id", required=True)
3195
+ caudit_p.add_argument("--limit", type=int, default=50)
3196
+ caudit_p.add_argument("--json", action="store_true", help="JSON output")
3197
+
3078
3198
  # -- desktop bridge (read-only, for NEXO Desktop and any external UI) --
3079
3199
  # v7.4.0 — lifecycle event bridge (guardian-claude-desktop-plan).
3080
3200
  lifecycle_parser = sub.add_parser("lifecycle", help="Conversation lifecycle event handler (v7.4 Desktop bridge)")
@@ -3268,6 +3388,24 @@ def main():
3268
3388
  return _clients_sync(args)
3269
3389
  clients_parser.print_help()
3270
3390
  return 0
3391
+ elif args.command == "mcp":
3392
+ if args.mcp_command == "status":
3393
+ return _mcp_status(args)
3394
+ if args.mcp_command == "clear-restart":
3395
+ return _mcp_clear_restart(args)
3396
+ mcp_parser.print_help()
3397
+ return 0
3398
+ elif args.command == "continuity":
3399
+ if args.continuity_command == "snapshot-write":
3400
+ return _continuity_snapshot_write(args)
3401
+ elif args.continuity_command == "snapshot-read":
3402
+ return _continuity_snapshot_read(args)
3403
+ elif args.continuity_command == "resume-bundle":
3404
+ return _continuity_resume_bundle(args)
3405
+ elif args.continuity_command == "audit":
3406
+ return _continuity_audit(args)
3407
+ continuity_parser.print_help()
3408
+ return 0
3271
3409
  elif args.command == "preferences":
3272
3410
  return _preferences(args)
3273
3411
  elif args.command == "doctor":
@@ -143,6 +143,45 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
143
143
  return DEFAULT_ASSISTANT_NAME
144
144
 
145
145
 
146
+ def _is_relative_to(path: Path, base: Path) -> bool:
147
+ try:
148
+ path.relative_to(base)
149
+ return True
150
+ except Exception:
151
+ return False
152
+
153
+
154
+ def _normalize_managed_runtime_root(nexo_home: Path, candidate: Path) -> Path:
155
+ raw = candidate.expanduser()
156
+ if raw.name == "server.py":
157
+ raw = raw.parent
158
+ try:
159
+ resolved = raw.resolve()
160
+ except Exception:
161
+ resolved = raw.absolute()
162
+ core_root = (nexo_home / "core").expanduser()
163
+ current_root = core_root / "current"
164
+ preferred_targets = []
165
+ if (core_root / "server.py").is_file():
166
+ preferred_targets.append(core_root)
167
+ if (current_root / "server.py").is_file():
168
+ preferred_targets.append(current_root)
169
+ if not preferred_targets:
170
+ return resolved
171
+ try:
172
+ core_resolved = core_root.resolve()
173
+ except Exception:
174
+ core_resolved = core_root.absolute()
175
+ version_root = core_resolved / "versions"
176
+ try:
177
+ current_resolved = current_root.resolve()
178
+ except Exception:
179
+ current_resolved = current_root.absolute()
180
+ if resolved == current_resolved or _is_relative_to(resolved, version_root):
181
+ return preferred_targets[0]
182
+ return resolved
183
+
184
+
146
185
  def _resolve_runtime_root(nexo_home: Path, runtime_root: str | os.PathLike[str] | None = None) -> Path:
147
186
  candidates: list[Path] = []
148
187
  if runtime_root:
@@ -155,12 +194,13 @@ def _resolve_runtime_root(nexo_home: Path, runtime_root: str | os.PathLike[str]
155
194
 
156
195
  seen: set[Path] = set()
157
196
  for candidate in candidates:
158
- resolved = candidate.resolve()
197
+ normalized = _normalize_managed_runtime_root(nexo_home, candidate)
198
+ resolved = normalized.resolve()
159
199
  if resolved in seen:
160
200
  continue
161
201
  seen.add(resolved)
162
- if (resolved / "server.py").is_file():
163
- return resolved
202
+ if (normalized / "server.py").is_file():
203
+ return normalized
164
204
  raise FileNotFoundError(f"Could not locate runtime root with server.py (tried {len(seen)} locations)")
165
205
 
166
206
 
@@ -409,6 +449,7 @@ def build_server_config(
409
449
  runtime_root: str | os.PathLike[str] | None = None,
410
450
  python_path: str = "",
411
451
  operator_name: str = "",
452
+ client: str = "",
412
453
  ) -> dict:
413
454
  nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
414
455
  runtime_root_path = _resolve_runtime_root(nexo_home_path, runtime_root)
@@ -423,6 +464,9 @@ def build_server_config(
423
464
  resolved_name = _resolve_operator_name(nexo_home_path, explicit=operator_name)
424
465
  if resolved_name:
425
466
  config["env"]["NEXO_NAME"] = resolved_name
467
+ resolved_client = normalize_client_key(client)
468
+ if resolved_client:
469
+ config["env"]["NEXO_MCP_CLIENT"] = resolved_client
426
470
  return config
427
471
 
428
472
 
@@ -436,6 +480,11 @@ def _claude_code_mcp_path(home: Path | None = None) -> Path:
436
480
  return base / ".claude.json"
437
481
 
438
482
 
483
+ def _claude_code_cortex_path(home: Path | None = None) -> Path:
484
+ base = home or _user_home()
485
+ return base / ".claude" / "mcp-cortex.json"
486
+
487
+
439
488
  def _claude_desktop_config_path(home: Path | None = None) -> Path:
440
489
  base = home or _user_home()
441
490
  if sys.platform == "darwin":
@@ -1022,6 +1071,7 @@ def sync_claude_code(
1022
1071
  runtime_root=runtime_root,
1023
1072
  python_path=python_path,
1024
1073
  operator_name=operator_name,
1074
+ client="claude_code",
1025
1075
  )
1026
1076
  home_path = Path(user_home).expanduser() if user_home else None
1027
1077
  result = _sync_claude_code_settings(
@@ -1039,6 +1089,13 @@ def sync_claude_code(
1039
1089
  )
1040
1090
  result["mcp"] = mcp_result
1041
1091
  result["mcp_path"] = mcp_result.get("path", "")
1092
+ cortex_result = _sync_json_client(
1093
+ _claude_code_cortex_path(home_path),
1094
+ server_config,
1095
+ "claude_code",
1096
+ )
1097
+ result["cortex_mcp"] = cortex_result
1098
+ result["cortex_mcp_path"] = cortex_result.get("path", "")
1042
1099
  bootstrap_result = sync_client_bootstrap(
1043
1100
  "claude_code",
1044
1101
  nexo_home=nexo_home,
@@ -1066,6 +1123,7 @@ def sync_claude_desktop(
1066
1123
  runtime_root=runtime_root,
1067
1124
  python_path=python_path,
1068
1125
  operator_name=operator_name,
1126
+ client="claude_desktop",
1069
1127
  )
1070
1128
  resolved_name = server_config.get("env", {}).get("NEXO_NAME", "") or _resolve_operator_name(
1071
1129
  Path(nexo_home).expanduser() if nexo_home else _default_nexo_home(),
@@ -1097,6 +1155,7 @@ def sync_codex(
1097
1155
  runtime_root=runtime_root,
1098
1156
  python_path=python_path,
1099
1157
  operator_name=operator_name,
1158
+ client="codex",
1100
1159
  )
1101
1160
  codex_bin = shutil.which("codex")
1102
1161
  config_path = _codex_config_path(home_path)