nexo-brain 2.6.7 → 2.6.10

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,7 +1,7 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.6.7",
4
- "description": "Local cognitive runtime for Claude Code persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
3
+ "version": "2.6.10",
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",
7
7
  "email": "info@nexo-brain.com",
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![GitHub stars](https://img.shields.io/github/stars/wazionapps/nexo?style=social)](https://github.com/wazionapps/nexo/stargazers)
7
7
  [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
8
8
 
9
- > Local cognitive runtime for Claude Code — persistent memory, overnight learning, runtime CLI, recovery-aware background jobs, startup preflight, and doctor diagnostics. 150+ MCP tools. Benchmarked on LoCoMo (F1 0.588, +55% vs GPT-4). Submitted to the Claude Code plugin marketplace.
9
+ > Local cognitive runtime for Claude Code — persistent memory, overnight learning, runtime CLI, recovery-aware background jobs, startup preflight, and doctor diagnostics. 150+ MCP tools. Benchmarked on LoCoMo (F1 0.588, +55% vs GPT-4). Claude Code plugin packaging included.
10
10
 
11
11
  **NEXO Brain transforms any MCP-compatible AI agent from a stateless assistant into a cognitive partner that remembers, learns, forgets, adapts, and builds a relationship with you over time.**
12
12
 
@@ -536,7 +536,7 @@ Files and areas that cause repeated errors accumulate a risk score (0.0–1.0).
536
536
  npx nexo-brain
537
537
  ```
538
538
 
539
- The installer handles everything:
539
+ The installer handles everything and syncs the same `nexo` MCP brain into Claude Code, Claude Desktop, and Codex when those clients are present:
540
540
 
541
541
  ```
542
542
  How should I call myself? (default: NEXO) > Atlas
@@ -570,6 +570,7 @@ After install, use the runtime CLI:
570
570
  nexo chat # Launch Claude Code with NEXO as operator
571
571
  nexo doctor # Check runtime health
572
572
  nexo update # Pull latest version and sync
573
+ nexo clients sync # Re-sync Claude Code/Desktop/Codex to the same brain
573
574
  nexo scripts list # See your personal scripts
574
575
  ```
575
576
 
@@ -583,7 +584,7 @@ Your operator will greet you immediately — adapted to the time of day, resumin
583
584
  |-----------|------|-------|
584
585
  | Cognitive engine | Python: fastembed, numpy, vector search | pip packages |
585
586
  | MCP server | 150+ tools for memory, cognition, learning, guard | NEXO_HOME/ |
586
- | Claude Code Plugin | Submitted to the Claude Code plugin marketplace (Anthropic) | `.claude-plugin/` |
587
+ | Claude Code Plugin | Marketplace-ready (packaging verified) | `.claude-plugin/` |
587
588
  | Plugins | Guard, episodic memory, cognitive memory, entities, preferences, update, etc. | Code: src/plugins/, Personal: NEXO_HOME/plugins/ |
588
589
  | Hooks (7) | SessionStart, Stop, PostToolUse, PreCompact, PostCompact | NEXO_HOME/hooks/ |
589
590
  | Nervous system | 13 core recovery-aware jobs + optional helpers (dashboard, prevent-sleep) | NEXO_HOME/scripts/ |
@@ -596,6 +597,7 @@ Your operator will greet you immediately — adapted to the time of day, resumin
596
597
  | Schedule config | schedule.json with customizable process times and timezone | NEXO_HOME/config/ |
597
598
  | Auto-update | Non-blocking startup check (5s max), opt-out via schedule.json | Built into server startup |
598
599
  | CLAUDE.md tracker | Version-tracked core sections with safe updates preserving customizations | Built into auto-update |
600
+ | Shared client sync | Same `nexo` MCP entry wired into Claude Code, Claude Desktop, and Codex | User config dirs |
599
601
  | Auto-diary | 3-layer system: PostToolUse every 10 calls, PreCompact emergency, heartbeat DIARY_OVERDUE | Built into hooks |
600
602
  | Claude Code config | MCP server + 7 hooks + 15 processes registered | ~/.claude/settings.json |
601
603
 
@@ -653,6 +655,7 @@ NEXO Brain separates **code** (immutable, in the repo or npm package) from **dat
653
655
  | `NEXO_HOME/data/` | SQLite databases (nexo.db, cognitive.db), migration state |
654
656
 
655
657
  The plugin loader scans `src/plugins/` first (base), then `NEXO_HOME/plugins/` (personal override by filename). This dual-directory approach lets you extend NEXO without forking the repo.
658
+ The client sync layer points Claude Code, Claude Desktop, and Codex at the same runtime and `NEXO_HOME`, so all three clients share one brain instead of drifting into separate local memories.
656
659
 
657
660
  ### 150+ MCP Tools across 21+ Categories
658
661
 
@@ -743,6 +746,14 @@ npx nexo-brain
743
746
 
744
747
  All 150+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically.
745
748
 
749
+ ### Claude Desktop
750
+
751
+ When Claude Desktop is installed, `nexo-brain`, `nexo update`, and `nexo clients sync` keep `claude_desktop_config.json` pointed at the same local NEXO runtime and `NEXO_HOME`.
752
+
753
+ ### Codex
754
+
755
+ When Codex CLI is available, `nexo-brain`, `nexo update`, and `nexo clients sync` register the same `nexo` MCP server via `codex mcp add`, so Codex uses the same local memory store as Claude Code and Claude Desktop.
756
+
746
757
  ### OpenClaw
747
758
 
748
759
  NEXO Brain also works as a cognitive memory backend for [OpenClaw](https://github.com/openclaw/openclaw):
@@ -812,7 +823,7 @@ NEXO Brain works with any application that supports the MCP protocol. Configure
812
823
  | mcpservers.org | MCP Directory | [mcpservers.org](https://mcpservers.org) |
813
824
  | OpenClaw | Native Plugin | [openclaw.com](https://openclaw.ai) |
814
825
  | dev.to | Technical Article | [How I Applied Cognitive Psychology to AI Agents](https://dev.to/wazionapps/how-i-applied-cognitive-psychology-to-give-ai-agents-real-memory-2oce) |
815
- | Claude Code | Plugin (pending review) | Submitted to Anthropic's plugin marketplace |
826
+ | Claude Code | Plugin (marketplace-ready) | Packaging verified, included in npm tarball |
816
827
  | nexo-brain.com | Official Website | [nexo-brain.com](https://nexo-brain.com) |
817
828
 
818
829
  ## Support the Project
@@ -828,6 +839,13 @@ If NEXO Brain is useful to you, consider:
828
839
 
829
840
  ## Changelog
830
841
 
842
+ ### v2.6.9 — Integration Sync, CI/CD Pipeline (2026-04-04)
843
+ - **Release artifact sync**: Automated version synchronization across Claude Code plugin, OpenClaw package, and ClawHub skill before every publish.
844
+ - **CI/CD pipeline**: Full GitHub Actions workflow for publish + verification of all integration channels.
845
+ - **OpenClaw plugin hardened**: Contract tests, correct runtime path, synchronized version. Published as @wazionapps/openclaw-memory-nexo-brain@2.6.9.
846
+ - **ClawHub skill hardened**: Version-synced metadata, correct server path, post-publish smoke verification.
847
+ - **Claude Code plugin packaging**: Verified plugin.json, .mcp.json, hooks included in npm tarball. Marketplace-ready.
848
+
831
849
  ### v2.6.5 — Power Helper Hardening, Recovery Contracts (2026-04-04)
832
850
  - Power helper semantics explicit and safer: `always_on` = platform helper for best-effort background availability.
833
851
  - Catch-up recovery suppresses duplicate relaunches for in-flight `cron_runs`.
@@ -915,7 +933,7 @@ If NEXO Brain is useful to you, consider:
915
933
  - **HNSW Vector Index**: Optional approximate nearest neighbor acceleration (auto-activates above 10,000 memories)
916
934
  - **Claim Graph**: Decomposes blob memories into atomic verifiable facts with provenance and contradiction detection
917
935
  - **Inter-terminal Auto-inbox (D+)**: `nexo_startup` accepts `claude_session_id` for automatic inbox delivery between parallel terminals
918
- - **Tests**: 24 pytest tests across 3 suites (cognitive, knowledge graph, migrations)
936
+ - **Tests**: 156 pytest tests across 3 suites (cognitive, knowledge graph, migrations)
919
937
 
920
938
  ### v1.4.1 — Multi-AI Code Review (2026-03-29)
921
939
  - **Fix**: 3 bugs found by GPT-5.4 (Codex CLI) + Gemini 2.5 (Gemini CLI) reviewing full codebase
package/bin/nexo-brain.js CHANGED
@@ -1764,6 +1764,7 @@ async function main() {
1764
1764
  "evolution_cycle.py",
1765
1765
  "migrate_embeddings.py",
1766
1766
  "auto_close_sessions.py",
1767
+ "client_sync.py",
1767
1768
  "auto_update.py",
1768
1769
  "tools_sessions.py",
1769
1770
  "tools_coordination.py",
@@ -2366,6 +2367,40 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
2366
2367
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
2367
2368
  log("MCP server + 8 core hooks configured in Claude Code settings.");
2368
2369
 
2370
+ const syncClientsScript = path.join(NEXO_HOME, "scripts", "nexo-sync-clients.py");
2371
+ if (fs.existsSync(syncClientsScript)) {
2372
+ const syncResult = spawnSync(
2373
+ python,
2374
+ [
2375
+ syncClientsScript,
2376
+ "--nexo-home", NEXO_HOME,
2377
+ "--runtime-root", NEXO_HOME,
2378
+ "--python", python,
2379
+ "--operator-name", operatorName,
2380
+ "--json",
2381
+ ],
2382
+ { encoding: "utf8" }
2383
+ );
2384
+ if (syncResult.status === 0) {
2385
+ try {
2386
+ const payload = JSON.parse(syncResult.stdout || "{}");
2387
+ const clients = payload.clients || {};
2388
+ const fmt = (name, key) => {
2389
+ const item = clients[key] || {};
2390
+ if (item.skipped) return `${name}: skipped`;
2391
+ if (item.ok) return `${name}: synced`;
2392
+ return `${name}: warning`;
2393
+ };
2394
+ log(`Shared brain client sync complete (${fmt("Claude Code", "claude_code")}, ${fmt("Claude Desktop", "claude_desktop")}, ${fmt("Codex", "codex")}).`);
2395
+ } catch {
2396
+ log("Shared brain client sync complete.");
2397
+ }
2398
+ } else {
2399
+ const errMsg = (syncResult.stderr || syncResult.stdout || "").trim();
2400
+ log(`WARN: shared brain client sync failed: ${errMsg || "unknown error"}`);
2401
+ }
2402
+ }
2403
+
2369
2404
  // Step 7: Create schedule.json (only on fresh install) and install core processes
2370
2405
  log("Setting up automated processes...");
2371
2406
  let schedule = loadOrCreateSchedule(NEXO_HOME);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.6.7",
3
+ "version": "2.6.10",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO — local cognitive runtime for Claude Code. Persistent memory, overnight learning, recovery-aware crons, personal scripts, doctor diagnostics, startup preflight, and optional power helper.",
6
6
  "bin": {
@@ -1197,6 +1197,7 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
1197
1197
  "server.py", "plugin_loader.py", "knowledge_graph.py", "kg_populate.py",
1198
1198
  "maintenance.py", "storage_router.py", "claim_graph.py", "hnsw_index.py",
1199
1199
  "evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
1200
+ "client_sync.py",
1200
1201
  "auto_update.py", "tools_sessions.py", "tools_coordination.py",
1201
1202
  "tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
1202
1203
  "tools_credentials.py", "tools_task_history.py", "tools_menu.py",
@@ -1244,6 +1245,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
1244
1245
  "server.py", "plugin_loader.py", "knowledge_graph.py", "kg_populate.py",
1245
1246
  "maintenance.py", "storage_router.py", "claim_graph.py", "hnsw_index.py",
1246
1247
  "evolution_cycle.py", "migrate_embeddings.py", "auto_close_sessions.py",
1248
+ "client_sync.py",
1247
1249
  "auto_update.py", "tools_sessions.py", "tools_coordination.py",
1248
1250
  "tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
1249
1251
  "tools_credentials.py", "tools_task_history.py", "tools_menu.py",
@@ -1432,6 +1434,18 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
1432
1434
  if power_result.get("ok"):
1433
1435
  actions.append(f"power:{power_result.get('action')}")
1434
1436
 
1437
+ _emit_progress(progress_fn, "Refreshing shared client configs...")
1438
+ try:
1439
+ from client_sync import sync_all_clients
1440
+
1441
+ client_sync_result = sync_all_clients(nexo_home=dest, runtime_root=dest)
1442
+ if client_sync_result.get("ok"):
1443
+ actions.append("client-sync")
1444
+ else:
1445
+ actions.append("client-sync-warning")
1446
+ except Exception as e:
1447
+ actions.append(f"client-sync-warning:{e}")
1448
+
1435
1449
  _emit_progress(progress_fn, "Verifying runtime imports...")
1436
1450
  verify = subprocess.run(
1437
1451
  [sys.executable, "-c", "import server"],
package/src/cli.py CHANGED
@@ -22,6 +22,7 @@ Entry points:
22
22
  nexo skills approve ID [--execution-level ...] [--approved-by ...] [--json]
23
23
  nexo skills featured [--limit N] [--json]
24
24
  nexo skills evolution [--json]
25
+ nexo clients sync [--json]
25
26
  nexo contributor status|on|off [--json]
26
27
  nexo doctor [--tier boot|runtime|deep|all] [--json] [--fix]
27
28
  """
@@ -569,6 +570,17 @@ def _update(args):
569
570
  return 0 if result.get("ok") else 1
570
571
 
571
572
 
573
+ def _clients_sync(args):
574
+ from client_sync import format_sync_summary, sync_all_clients
575
+
576
+ result = sync_all_clients(nexo_home=NEXO_HOME, runtime_root=NEXO_CODE)
577
+ if args.json:
578
+ print(json.dumps(result, indent=2, ensure_ascii=False))
579
+ else:
580
+ print(format_sync_summary(result))
581
+ return 0 if result.get("ok") else 1
582
+
583
+
572
584
  def _contributor_status(args):
573
585
  from public_contribution import (
574
586
  format_public_contribution_label,
@@ -861,6 +873,7 @@ Commands:
861
873
  nexo scripts list|create|classify|sync|reconcile|ensure-schedules|schedules|run|doctor|call|unschedule|remove
862
874
  Personal scripts
863
875
  nexo skills list|apply|sync|approve Executable skills
876
+ nexo clients sync Sync Claude Code/Desktop/Codex MCP configs
864
877
  nexo update Update installed runtime
865
878
  nexo contributor status|on|off Public Draft PR contribution mode
866
879
  nexo dashboard on|off|status Web dashboard control
@@ -950,6 +963,12 @@ def main():
950
963
  update_parser = sub.add_parser("update", help="Update installed runtime")
951
964
  update_parser.add_argument("--json", action="store_true", help="JSON output")
952
965
 
966
+ # -- clients --
967
+ clients_parser = sub.add_parser("clients", help="Shared client config management")
968
+ clients_sub = clients_parser.add_subparsers(dest="clients_command")
969
+ clients_sync_p = clients_sub.add_parser("sync", help="Sync Claude Code, Claude Desktop, and Codex to the same NEXO brain")
970
+ clients_sync_p.add_argument("--json", action="store_true", help="JSON output")
971
+
953
972
  # -- doctor --
954
973
  doctor_parser = sub.add_parser("doctor", help="Unified diagnostics")
955
974
  doctor_parser.add_argument("--tier", default="boot", choices=["boot", "runtime", "deep", "all"],
@@ -1045,6 +1064,11 @@ def main():
1045
1064
  return _chat(args)
1046
1065
  elif args.command == "update":
1047
1066
  return _update(args)
1067
+ elif args.command == "clients":
1068
+ if args.clients_command == "sync":
1069
+ return _clients_sync(args)
1070
+ clients_parser.print_help()
1071
+ return 0
1048
1072
  elif args.command == "doctor":
1049
1073
  return _doctor(args)
1050
1074
  elif args.command == "contributor":
@@ -0,0 +1,327 @@
1
+ from __future__ import annotations
2
+
3
+ """Shared client sync for Claude Code, Claude Desktop, and Codex."""
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ def _user_home() -> Path:
15
+ return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
16
+
17
+
18
+ def _default_nexo_home() -> Path:
19
+ return Path(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo"))).expanduser()
20
+
21
+
22
+ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
23
+ explicit = (explicit or "").strip()
24
+ if explicit:
25
+ return explicit
26
+ env_name = os.environ.get("NEXO_NAME", "").strip()
27
+ if env_name:
28
+ return env_name
29
+ version_file = nexo_home / "version.json"
30
+ if version_file.is_file():
31
+ try:
32
+ return str(json.loads(version_file.read_text()).get("operator_name", "")).strip()
33
+ except Exception:
34
+ pass
35
+ return ""
36
+
37
+
38
+ def _resolve_runtime_root(nexo_home: Path, runtime_root: str | os.PathLike[str] | None = None) -> Path:
39
+ candidates: list[Path] = []
40
+ if runtime_root:
41
+ candidates.append(Path(runtime_root).expanduser())
42
+ code_env = os.environ.get("NEXO_CODE", "").strip()
43
+ if code_env:
44
+ code_path = Path(code_env).expanduser()
45
+ candidates.extend([code_path, code_path / "src"])
46
+ candidates.extend([nexo_home, Path.cwd(), Path.cwd() / "src"])
47
+
48
+ seen: set[Path] = set()
49
+ for candidate in candidates:
50
+ resolved = candidate.resolve()
51
+ if resolved in seen:
52
+ continue
53
+ seen.add(resolved)
54
+ if (resolved / "server.py").is_file():
55
+ return resolved
56
+ raise FileNotFoundError(f"Could not locate runtime root with server.py (tried {len(seen)} locations)")
57
+
58
+
59
+ def _resolve_python(nexo_home: Path, explicit: str = "") -> str:
60
+ candidates = [
61
+ explicit,
62
+ str(nexo_home / ".venv" / "bin" / "python3"),
63
+ str(nexo_home / ".venv" / "bin" / "python"),
64
+ str(nexo_home / ".venv" / "Scripts" / "python.exe"),
65
+ shutil.which("python3") or "",
66
+ shutil.which("python") or "",
67
+ sys.executable,
68
+ ]
69
+ for candidate in candidates:
70
+ if candidate and Path(candidate).exists():
71
+ return str(Path(candidate))
72
+ return explicit or sys.executable
73
+
74
+
75
+ def build_server_config(
76
+ *,
77
+ nexo_home: str | os.PathLike[str] | None = None,
78
+ runtime_root: str | os.PathLike[str] | None = None,
79
+ python_path: str = "",
80
+ operator_name: str = "",
81
+ ) -> dict:
82
+ nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
83
+ runtime_root_path = _resolve_runtime_root(nexo_home_path, runtime_root)
84
+ config = {
85
+ "command": _resolve_python(nexo_home_path, python_path),
86
+ "args": [str(runtime_root_path / "server.py")],
87
+ "env": {
88
+ "NEXO_HOME": str(nexo_home_path),
89
+ "NEXO_CODE": str(runtime_root_path),
90
+ },
91
+ }
92
+ resolved_name = _resolve_operator_name(nexo_home_path, explicit=operator_name)
93
+ if resolved_name:
94
+ config["env"]["NEXO_NAME"] = resolved_name
95
+ return config
96
+
97
+
98
+ def _claude_code_settings_path(home: Path | None = None) -> Path:
99
+ base = home or _user_home()
100
+ return base / ".claude" / "settings.json"
101
+
102
+
103
+ def _claude_desktop_config_path(home: Path | None = None) -> Path:
104
+ base = home or _user_home()
105
+ if sys.platform == "darwin":
106
+ return base / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
107
+ if os.name == "nt":
108
+ return base / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
109
+ return base / ".config" / "Claude" / "claude_desktop_config.json"
110
+
111
+
112
+ def _codex_config_path(home: Path | None = None) -> Path:
113
+ base = home or _user_home()
114
+ return base / ".codex" / "config.toml"
115
+
116
+
117
+ def _load_json_object(path: Path) -> dict:
118
+ if not path.is_file():
119
+ return {}
120
+ try:
121
+ data = json.loads(path.read_text())
122
+ except Exception as exc:
123
+ raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
124
+ if not isinstance(data, dict):
125
+ raise ValueError(f"Expected JSON object in {path}")
126
+ return data
127
+
128
+
129
+ def _write_json_object(path: Path, payload: dict) -> None:
130
+ path.parent.mkdir(parents=True, exist_ok=True)
131
+ path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
132
+
133
+
134
+ def _sync_json_client(path: Path, server_config: dict, label: str) -> dict:
135
+ payload = _load_json_object(path)
136
+ mcp_servers = payload.setdefault("mcpServers", {})
137
+ if not isinstance(mcp_servers, dict):
138
+ mcp_servers = {}
139
+ payload["mcpServers"] = mcp_servers
140
+ action = "updated" if "nexo" in mcp_servers else "created"
141
+ mcp_servers["nexo"] = server_config
142
+ _write_json_object(path, payload)
143
+ return {
144
+ "ok": True,
145
+ "client": label,
146
+ "action": action,
147
+ "path": str(path),
148
+ }
149
+
150
+
151
+ def sync_claude_code(
152
+ *,
153
+ nexo_home: str | os.PathLike[str] | None = None,
154
+ runtime_root: str | os.PathLike[str] | None = None,
155
+ python_path: str = "",
156
+ operator_name: str = "",
157
+ user_home: str | os.PathLike[str] | None = None,
158
+ ) -> dict:
159
+ server_config = build_server_config(
160
+ nexo_home=nexo_home,
161
+ runtime_root=runtime_root,
162
+ python_path=python_path,
163
+ operator_name=operator_name,
164
+ )
165
+ return _sync_json_client(
166
+ _claude_code_settings_path(Path(user_home).expanduser() if user_home else None),
167
+ server_config,
168
+ "claude_code",
169
+ )
170
+
171
+
172
+ def sync_claude_desktop(
173
+ *,
174
+ nexo_home: str | os.PathLike[str] | None = None,
175
+ runtime_root: str | os.PathLike[str] | None = None,
176
+ python_path: str = "",
177
+ operator_name: str = "",
178
+ user_home: str | os.PathLike[str] | None = None,
179
+ ) -> dict:
180
+ server_config = build_server_config(
181
+ nexo_home=nexo_home,
182
+ runtime_root=runtime_root,
183
+ python_path=python_path,
184
+ operator_name=operator_name,
185
+ )
186
+ return _sync_json_client(
187
+ _claude_desktop_config_path(Path(user_home).expanduser() if user_home else None),
188
+ server_config,
189
+ "claude_desktop",
190
+ )
191
+
192
+
193
+ def sync_codex(
194
+ *,
195
+ nexo_home: str | os.PathLike[str] | None = None,
196
+ runtime_root: str | os.PathLike[str] | None = None,
197
+ python_path: str = "",
198
+ operator_name: str = "",
199
+ user_home: str | os.PathLike[str] | None = None,
200
+ ) -> dict:
201
+ nexo_home_path = Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()
202
+ home_path = Path(user_home).expanduser() if user_home else _user_home()
203
+ server_config = build_server_config(
204
+ nexo_home=nexo_home_path,
205
+ runtime_root=runtime_root,
206
+ python_path=python_path,
207
+ operator_name=operator_name,
208
+ )
209
+ codex_bin = shutil.which("codex")
210
+ config_path = _codex_config_path(home_path)
211
+ if not codex_bin:
212
+ return {
213
+ "ok": True,
214
+ "client": "codex",
215
+ "skipped": True,
216
+ "reason": "codex binary not found in PATH",
217
+ "path": str(config_path),
218
+ }
219
+
220
+ cmd = [codex_bin, "mcp", "add", "nexo"]
221
+ for key, value in sorted(server_config.get("env", {}).items()):
222
+ cmd.extend(["--env", f"{key}={value}"])
223
+ cmd.extend(["--", server_config["command"], *server_config.get("args", [])])
224
+ env = {**os.environ, "HOME": str(home_path)}
225
+ result = subprocess.run(
226
+ cmd,
227
+ capture_output=True,
228
+ text=True,
229
+ timeout=30,
230
+ env=env,
231
+ )
232
+ if result.returncode != 0:
233
+ return {
234
+ "ok": False,
235
+ "client": "codex",
236
+ "path": str(config_path),
237
+ "error": (result.stderr or result.stdout or "codex mcp add failed").strip(),
238
+ }
239
+ return {
240
+ "ok": True,
241
+ "client": "codex",
242
+ "action": "updated",
243
+ "path": str(config_path),
244
+ "mode": "cli",
245
+ }
246
+
247
+
248
+ def sync_all_clients(
249
+ *,
250
+ nexo_home: str | os.PathLike[str] | None = None,
251
+ runtime_root: str | os.PathLike[str] | None = None,
252
+ python_path: str = "",
253
+ operator_name: str = "",
254
+ user_home: str | os.PathLike[str] | None = None,
255
+ ) -> dict:
256
+ def _safe(label: str, fn) -> dict:
257
+ try:
258
+ return fn(
259
+ nexo_home=nexo_home,
260
+ runtime_root=runtime_root,
261
+ python_path=python_path,
262
+ operator_name=operator_name,
263
+ user_home=user_home,
264
+ )
265
+ except Exception as exc:
266
+ return {"ok": False, "client": label, "error": str(exc)}
267
+
268
+ results = {
269
+ "claude_code": _safe("claude_code", sync_claude_code),
270
+ "claude_desktop": _safe("claude_desktop", sync_claude_desktop),
271
+ "codex": _safe("codex", sync_codex),
272
+ }
273
+ ok = all(item.get("ok") or item.get("skipped") for item in results.values())
274
+ return {
275
+ "ok": ok,
276
+ "nexo_home": str(Path(nexo_home).expanduser() if nexo_home else _default_nexo_home()),
277
+ "runtime_root": str(_resolve_runtime_root(
278
+ Path(nexo_home).expanduser() if nexo_home else _default_nexo_home(),
279
+ runtime_root,
280
+ )),
281
+ "clients": results,
282
+ }
283
+
284
+
285
+ def format_sync_summary(result: dict) -> str:
286
+ labels = {
287
+ "claude_code": "Claude Code",
288
+ "claude_desktop": "Claude Desktop",
289
+ "codex": "Codex",
290
+ }
291
+ lines = ["SHARED BRAIN SYNC"]
292
+ for key in ["claude_code", "claude_desktop", "codex"]:
293
+ item = result.get("clients", {}).get(key, {})
294
+ label = labels[key]
295
+ if item.get("skipped"):
296
+ lines.append(f" {label}: skipped ({item.get('reason', 'not available')})")
297
+ elif item.get("ok"):
298
+ lines.append(f" {label}: {item.get('action', 'synced')} -> {item.get('path', '')}")
299
+ else:
300
+ lines.append(f" {label}: ERROR -> {item.get('error', 'unknown error')}")
301
+ return "\n".join(lines)
302
+
303
+
304
+ def main(argv: list[str] | None = None) -> int:
305
+ parser = argparse.ArgumentParser(description="Sync NEXO MCP config across Claude Code, Claude Desktop, and Codex.")
306
+ parser.add_argument("--nexo-home", default=str(_default_nexo_home()))
307
+ parser.add_argument("--runtime-root", default="")
308
+ parser.add_argument("--python", dest="python_path", default="")
309
+ parser.add_argument("--operator-name", default="")
310
+ parser.add_argument("--json", action="store_true")
311
+ args = parser.parse_args(argv)
312
+
313
+ result = sync_all_clients(
314
+ nexo_home=args.nexo_home,
315
+ runtime_root=args.runtime_root or None,
316
+ python_path=args.python_path,
317
+ operator_name=args.operator_name,
318
+ )
319
+ if args.json:
320
+ print(json.dumps(result, indent=2, ensure_ascii=False))
321
+ else:
322
+ print(format_sync_summary(result))
323
+ return 0 if result.get("ok") else 1
324
+
325
+
326
+ if __name__ == "__main__":
327
+ raise SystemExit(main())
@@ -584,6 +584,21 @@ def handle_update(remote: str = "origin", branch: str = "main", progress_fn=None
584
584
  except Exception as e:
585
585
  pass # Non-critical, log in function
586
586
 
587
+ # Step 10: Sync shared client configs
588
+ try:
589
+ _emit_progress(progress_fn, "Refreshing shared client configs...")
590
+ from client_sync import sync_all_clients
591
+
592
+ client_sync_result = sync_all_clients(
593
+ nexo_home=NEXO_HOME,
594
+ runtime_root=SRC_DIR,
595
+ operator_name=os.environ.get("NEXO_NAME", ""),
596
+ )
597
+ if client_sync_result.get("ok"):
598
+ steps_done.append("client-sync")
599
+ except Exception:
600
+ pass # Non-critical, configs can be re-synced later
601
+
587
602
  # Build result
588
603
  if pull_out == "Already up to date.":
589
604
  return f"Already up to date (v{old_version}). No changes pulled."
@@ -603,6 +618,8 @@ def handle_update(remote: str = "origin", branch: str = "main", progress_fn=None
603
618
  lines.append(" Crons: synced with manifest")
604
619
  if "hook-sync" in steps_done:
605
620
  lines.append(" Hooks: synced to NEXO_HOME")
621
+ if "client-sync" in steps_done:
622
+ lines.append(" Clients: Claude Code/Desktop/Codex synced")
606
623
  lines.append("")
607
624
  lines.append("MCP server restart needed to load new code.")
608
625
  return "\n".join(lines)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ if str(ROOT) not in sys.path:
10
+ sys.path.insert(0, str(ROOT))
11
+
12
+ from client_sync import main
13
+
14
+
15
+ if __name__ == "__main__":
16
+ raise SystemExit(main())
@@ -247,6 +247,17 @@ if $CRON_SYNC_OK && [ -d "$SRC_DIR/crons" ]; then
247
247
  log "Refreshed installed crons manifest."
248
248
  fi
249
249
 
250
+ # --- Step 9: Sync shared client configs ---
251
+ CLIENT_SYNC="$SRC_DIR/scripts/nexo-sync-clients.py"
252
+ if [ -f "$CLIENT_SYNC" ]; then
253
+ log "Syncing Claude Code, Claude Desktop, and Codex configs..."
254
+ if NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 "$CLIENT_SYNC" --nexo-home "$NEXO_HOME" --runtime-root "$SRC_DIR" --json >/dev/null 2>&1; then
255
+ log "Shared client configs synced."
256
+ else
257
+ warn "Client config sync failed (non-fatal). Run 'nexo clients sync' later."
258
+ fi
259
+ fi
260
+
250
261
  # --- Done ---
251
262
  echo ""
252
263
  log "========================================="