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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/bin/nexo-brain.js +9 -0
- package/package.json +1 -1
- package/src/auto_update.py +41 -30
- package/src/bootstrap_docs.py +8 -0
- package/src/cli.py +138 -0
- package/src/client_sync.py +62 -3
- package/src/continuity.py +442 -0
- package/src/core_prompts.py +20 -8
- package/src/db/__init__.py +8 -0
- package/src/db/_continuity.py +171 -0
- package/src/db/_schema.py +33 -0
- package/src/db/_sessions.py +23 -7
- package/src/lifecycle_events.py +47 -2
- package/src/paths.py +24 -1
- package/src/plugin_loader.py +7 -1
- package/src/plugins/update.py +49 -0
- package/src/runtime_versioning.py +361 -0
- package/src/server.py +122 -1
- package/src/tools_sessions.py +30 -0
- package/tool-enforcement-map.json +75 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
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.
|
|
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.
|
|
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",
|
package/src/auto_update.py
CHANGED
|
@@ -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",
|
|
2258
|
-
("skills-core",
|
|
2259
|
-
("db",
|
|
2260
|
-
("cognitive",
|
|
2261
|
-
("doctor",
|
|
2262
|
-
("dashboard",
|
|
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",
|
|
2267
|
-
("hooks",
|
|
2268
|
-
("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",
|
|
2418
|
-
("cognitive",
|
|
2419
|
-
("doctor",
|
|
2420
|
-
("dashboard",
|
|
2421
|
-
("skills-core",
|
|
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",
|
|
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",
|
|
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
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
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:
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -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":
|
package/src/client_sync.py
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
163
|
-
return
|
|
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)
|