nexo-brain 2.6.9 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -5
- package/bin/nexo-brain.js +35 -0
- package/package.json +1 -1
- package/src/auto_update.py +14 -0
- package/src/cli.py +24 -0
- package/src/client_sync.py +327 -0
- package/src/plugins/update.py +17 -0
- package/src/scripts/nexo-sync-clients.py +16 -0
- package/src/scripts/nexo-update.sh +11 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.10",
|
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/wazionapps/nexo/stargazers)
|
|
7
7
|
[](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).
|
|
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 |
|
|
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 (
|
|
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**:
|
|
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.
|
|
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": {
|
package/src/auto_update.py
CHANGED
|
@@ -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())
|
package/src/plugins/update.py
CHANGED
|
@@ -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 "========================================="
|