nexo-brain 5.2.1 → 5.3.1
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 +9 -0
- package/bin/nexo-brain.js +30 -4
- package/bin/nexo.js +43 -7
- package/package.json +2 -1
- package/scripts/sync_release_artifacts.py +128 -0
- package/src/auto_update.py +66 -8
- package/src/bootstrap_docs.py +53 -2
- package/src/cli.py +225 -1
- package/src/client_sync.py +2 -1
- package/src/db/_personal_scripts.py +30 -8
- package/src/doctor/providers/runtime.py +37 -1
- package/src/hooks/session-start.sh +8 -4
- package/src/plugins/update.py +90 -3
- package/src/runtime_home.py +46 -0
- package/src/script_registry.py +18 -6
- package/src/scripts/nexo-cognitive-decay.py +1 -1
- package/src/scripts/nexo-cortex-cycle.py +2 -2
- package/src/scripts/nexo-hook-record.py +1 -1
- package/src/scripts/nexo-migrate.py +38 -10
- package/src/state_watchers_runtime.py +1 -1
- package/templates/CLAUDE.md.template +7 -1
- package/templates/CODEX.AGENTS.md.template +7 -1
- package/templates/nexo_helper.py +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
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
|
@@ -87,6 +87,15 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
|
|
|
87
87
|
- when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
|
|
88
88
|
- NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
|
|
89
89
|
|
|
90
|
+
Version `5.3.1` normalizes packaged npm installs so they behave like packaged npm installs: `nexo update` now keeps the runtime anchored to `~/.nexo`, refreshes packaged bootstrap/client artifacts after upgrade, avoids repo-only release-artifact drift in installed runtimes, and keeps personal scripts on the canonical packaged path.
|
|
91
|
+
|
|
92
|
+
Version `5.3.0` adds `nexo uninstall` — a CLI command that cleanly separates runtime from user data. It stops all crons, removes the MCP server config, and preserves databases, learnings, and personal scripts for safe reinstall.
|
|
93
|
+
|
|
94
|
+
Version `5.2.1` fixes the Deep Sleep datetime regression and closes the decision-to-outcome feedback gap:
|
|
95
|
+
|
|
96
|
+
- `_parse_any_datetime` in `apply_findings.py` now strips timezone info before comparison, fixing the offset-aware/offset-naive crash that was breaking Deep Sleep verification work.
|
|
97
|
+
- `cortex_decide()` now auto-creates a `decision_outcome` when none is linked yet, so the outcome-checker cron can verify real decisions instead of leaving the loop open.
|
|
98
|
+
|
|
90
99
|
Version `5.2.0` closes two focused gaps in the Cortex layer that were left open by the v5.1 audit — the high-stakes response-contract detector was English-only, and the `nexo-cortex-cycle` cron was writing a quality snapshot that no reader ever consumed:
|
|
91
100
|
|
|
92
101
|
- `HIGH_STAKES_KEYWORDS_ES` adds ~45 Spanish keywords to the high-stakes detector with accented and unaccented variants, so a goal written in Spanish (`migrar la base de datos de producción`) trips the same gate as its English twin.
|
package/bin/nexo-brain.js
CHANGED
|
@@ -2210,15 +2210,31 @@ async function main() {
|
|
|
2210
2210
|
"#!/usr/bin/env bash",
|
|
2211
2211
|
"set -euo pipefail",
|
|
2212
2212
|
"",
|
|
2213
|
-
`
|
|
2213
|
+
`RUNTIME_HOME="${NEXO_HOME}"`,
|
|
2214
|
+
'NEXO_HOME="$RUNTIME_HOME"',
|
|
2214
2215
|
'export NEXO_HOME',
|
|
2215
|
-
'
|
|
2216
|
+
'resolve_code_dir() {',
|
|
2217
|
+
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then',
|
|
2218
|
+
' printf \'%s\\n\' "${NEXO_CODE%/}"',
|
|
2219
|
+
' return 0',
|
|
2220
|
+
' fi',
|
|
2221
|
+
' if [ -f "$NEXO_HOME/cli.py" ]; then',
|
|
2222
|
+
' printf \'%s\\n\' "$NEXO_HOME"',
|
|
2223
|
+
' return 0',
|
|
2224
|
+
' fi',
|
|
2225
|
+
' printf \'%s\\n\' "$NEXO_HOME"',
|
|
2226
|
+
'}',
|
|
2227
|
+
'NEXO_CODE="$(resolve_code_dir)"',
|
|
2228
|
+
'export NEXO_CODE',
|
|
2216
2229
|
'resolve_python() {',
|
|
2217
2230
|
' local candidates=()',
|
|
2218
2231
|
' local candidate=""',
|
|
2219
2232
|
' if [ -n "${NEXO_RUNTIME_PYTHON:-}" ]; then candidates+=("$NEXO_RUNTIME_PYTHON"); fi',
|
|
2220
2233
|
' if [ -n "${NEXO_PYTHON:-}" ]; then candidates+=("$NEXO_PYTHON"); fi',
|
|
2221
|
-
' candidates+=("$
|
|
2234
|
+
' candidates+=("$NEXO_CODE/.venv/bin/python3" "$NEXO_CODE/.venv/bin/python")',
|
|
2235
|
+
' if [ "$NEXO_CODE" != "$NEXO_HOME" ]; then',
|
|
2236
|
+
' candidates+=("$NEXO_HOME/.venv/bin/python3" "$NEXO_HOME/.venv/bin/python")',
|
|
2237
|
+
' fi',
|
|
2222
2238
|
' case "$(uname -s)" in',
|
|
2223
2239
|
' Darwin) candidates+=("/opt/homebrew/bin/python3" "/usr/local/bin/python3") ;;',
|
|
2224
2240
|
' *) candidates+=("/usr/local/bin/python3" "/usr/bin/python3") ;;',
|
|
@@ -2246,7 +2262,17 @@ async function main() {
|
|
|
2246
2262
|
' echo "NEXO runtime Python not found. Run nexo-brain or nexo update to repair the installation." >&2',
|
|
2247
2263
|
' exit 1',
|
|
2248
2264
|
'fi',
|
|
2249
|
-
'
|
|
2265
|
+
'CLI_PY="$NEXO_CODE/cli.py"',
|
|
2266
|
+
'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/cli.py" ]; then',
|
|
2267
|
+
' NEXO_CODE="$NEXO_HOME"',
|
|
2268
|
+
' export NEXO_CODE',
|
|
2269
|
+
' CLI_PY="$NEXO_HOME/cli.py"',
|
|
2270
|
+
'fi',
|
|
2271
|
+
'if [ ! -f "$CLI_PY" ]; then',
|
|
2272
|
+
' echo "NEXO CLI not found under $NEXO_HOME. Run nexo-brain or nexo update to repair the installation." >&2',
|
|
2273
|
+
' exit 1',
|
|
2274
|
+
'fi',
|
|
2275
|
+
'exec "$PYTHON" "$CLI_PY" "$@"',
|
|
2250
2276
|
"",
|
|
2251
2277
|
].join("\n");
|
|
2252
2278
|
const runtimeCliPath = path.join(NEXO_HOME, "bin", "nexo");
|
package/bin/nexo.js
CHANGED
|
@@ -12,7 +12,44 @@ const fs = require("fs");
|
|
|
12
12
|
const os = require("os");
|
|
13
13
|
const path = require("path");
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function resolveNexoHome(rawValue) {
|
|
16
|
+
const homeDir = os.homedir();
|
|
17
|
+
const managedHome = path.join(homeDir, ".nexo");
|
|
18
|
+
const legacyHome = path.join(homeDir, "claude");
|
|
19
|
+
const candidate = rawValue || managedHome;
|
|
20
|
+
|
|
21
|
+
if (candidate === managedHome) return managedHome;
|
|
22
|
+
if (candidate === legacyHome) return fs.existsSync(managedHome) ? managedHome : candidate;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(managedHome) && fs.realpathSync.native(candidate) === fs.realpathSync.native(managedHome)) {
|
|
26
|
+
return managedHome;
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const NEXO_HOME = resolveNexoHome(process.env.NEXO_HOME);
|
|
34
|
+
const NEXO_CODE = resolveCodeDir();
|
|
35
|
+
|
|
36
|
+
function resolveCodeDir() {
|
|
37
|
+
const envCode = process.env.NEXO_CODE;
|
|
38
|
+
if (envCode && fs.existsSync(path.join(envCode, "cli.py"))) {
|
|
39
|
+
return envCode;
|
|
40
|
+
}
|
|
41
|
+
const repoCandidate = path.join(__dirname, "..", "src", "cli.py");
|
|
42
|
+
if (fs.existsSync(repoCandidate)) {
|
|
43
|
+
return path.join(__dirname, "..", "src");
|
|
44
|
+
}
|
|
45
|
+
if (fs.existsSync(path.join(NEXO_HOME, "cli.py"))) {
|
|
46
|
+
return NEXO_HOME;
|
|
47
|
+
}
|
|
48
|
+
if (fs.existsSync(path.join(NEXO_HOME, "claude", "cli.py"))) {
|
|
49
|
+
return path.join(NEXO_HOME, "claude");
|
|
50
|
+
}
|
|
51
|
+
return NEXO_HOME;
|
|
52
|
+
}
|
|
16
53
|
|
|
17
54
|
function pythonSupportsModule(candidate, moduleName) {
|
|
18
55
|
if (!candidate) return false;
|
|
@@ -23,7 +60,7 @@ function pythonSupportsModule(candidate, moduleName) {
|
|
|
23
60
|
env: {
|
|
24
61
|
...process.env,
|
|
25
62
|
NEXO_HOME,
|
|
26
|
-
NEXO_CODE
|
|
63
|
+
NEXO_CODE,
|
|
27
64
|
},
|
|
28
65
|
});
|
|
29
66
|
return result.status === 0;
|
|
@@ -36,6 +73,8 @@ function findPython() {
|
|
|
36
73
|
const candidates = [
|
|
37
74
|
process.env.NEXO_RUNTIME_PYTHON,
|
|
38
75
|
process.env.NEXO_PYTHON,
|
|
76
|
+
path.join(NEXO_CODE, ".venv", "bin", "python3"),
|
|
77
|
+
path.join(NEXO_CODE, ".venv", "bin", "python"),
|
|
39
78
|
path.join(NEXO_HOME, ".venv", "bin", "python3"),
|
|
40
79
|
path.join(NEXO_HOME, ".venv", "bin", "python"),
|
|
41
80
|
process.platform === "darwin" ? "/opt/homebrew/bin/python3" : "",
|
|
@@ -55,10 +94,7 @@ function findPython() {
|
|
|
55
94
|
}
|
|
56
95
|
|
|
57
96
|
function findCliPy() {
|
|
58
|
-
|
|
59
|
-
const installedCandidate = path.join(NEXO_HOME, "cli.py");
|
|
60
|
-
if (fs.existsSync(repoCandidate)) return repoCandidate;
|
|
61
|
-
return installedCandidate;
|
|
97
|
+
return path.join(NEXO_CODE, "cli.py");
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
const python = findPython();
|
|
@@ -75,7 +111,7 @@ const result = spawnSync(python, [cliPy, ...process.argv.slice(2)], {
|
|
|
75
111
|
env: {
|
|
76
112
|
...process.env,
|
|
77
113
|
NEXO_HOME,
|
|
78
|
-
NEXO_CODE
|
|
114
|
+
NEXO_CODE,
|
|
79
115
|
},
|
|
80
116
|
});
|
|
81
117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
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",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"bin/nexo-brain.js",
|
|
68
68
|
"bin/nexo.js",
|
|
69
69
|
"bin/postinstall.js",
|
|
70
|
+
"scripts/sync_release_artifacts.py",
|
|
70
71
|
"src/",
|
|
71
72
|
"community/",
|
|
72
73
|
"!src/**/__pycache__",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
ROOT_PACKAGE_JSON = ROOT / "package.json"
|
|
12
|
+
CLAUDE_PLUGIN_JSON = ROOT / ".claude-plugin" / "plugin.json"
|
|
13
|
+
CLAWHUB_SKILL_MD = ROOT / "clawhub-skill" / "SKILL.md"
|
|
14
|
+
OPENCLAW_PACKAGE_JSON = ROOT / "openclaw-plugin" / "package.json"
|
|
15
|
+
OPENCLAW_MCP_BRIDGE = ROOT / "openclaw-plugin" / "src" / "mcp-bridge.ts"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fail(message: str) -> None:
|
|
19
|
+
raise SystemExit(f"[sync-release-artifacts] {message}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_json(path: Path) -> dict:
|
|
23
|
+
return json.loads(path.read_text())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dump_json(path: Path, payload: dict) -> None:
|
|
27
|
+
path.write_text(json.dumps(payload, indent=2) + "\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sync_json_version(path: Path, expected_version: str, label: str) -> bool:
|
|
31
|
+
payload = load_json(path)
|
|
32
|
+
if payload.get("version") == expected_version:
|
|
33
|
+
return False
|
|
34
|
+
payload["version"] = expected_version
|
|
35
|
+
dump_json(path, payload)
|
|
36
|
+
print(f"[sync-release-artifacts] synced {label} version -> {expected_version}")
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sync_clawhub_skill(skill_path: Path, expected_version: str) -> bool:
|
|
41
|
+
text = skill_path.read_text()
|
|
42
|
+
updated = text
|
|
43
|
+
|
|
44
|
+
updated = re.sub(
|
|
45
|
+
r"(?m)^version:\s*[^\n]+$",
|
|
46
|
+
f"version: {expected_version}",
|
|
47
|
+
updated,
|
|
48
|
+
count=1,
|
|
49
|
+
)
|
|
50
|
+
updated = updated.replace("~/.nexo/src/server.py", "~/.nexo/server.py")
|
|
51
|
+
|
|
52
|
+
if updated == text:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
skill_path.write_text(updated)
|
|
56
|
+
print(f"[sync-release-artifacts] synced ClawHub skill -> {expected_version}")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sync_openclaw_bridge(bridge_path: Path, expected_version: str) -> bool:
|
|
61
|
+
text = bridge_path.read_text()
|
|
62
|
+
updated = text
|
|
63
|
+
|
|
64
|
+
updated = updated.replace(
|
|
65
|
+
'resolve(this.config.nexoHome, "src", "server.py")',
|
|
66
|
+
'resolve(this.config.nexoHome, "server.py")',
|
|
67
|
+
)
|
|
68
|
+
updated = re.sub(
|
|
69
|
+
r'clientInfo:\s*\{\s*name:\s*"openclaw-memory-nexo-brain",\s*version:\s*"[^"]+"\s*\}',
|
|
70
|
+
f'clientInfo: {{ name: "openclaw-memory-nexo-brain", version: "{expected_version}" }}',
|
|
71
|
+
updated,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if updated == text:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
bridge_path.write_text(updated)
|
|
78
|
+
print(f"[sync-release-artifacts] synced OpenClaw bridge -> {expected_version}")
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> None:
|
|
83
|
+
parser = argparse.ArgumentParser(description="Keep release-facing integration artifacts in sync.")
|
|
84
|
+
parser.add_argument("--release-version", help="Expected release version (must match root package.json).")
|
|
85
|
+
parser.add_argument("--check", action="store_true", help="Fail if any artifact would change.")
|
|
86
|
+
args = parser.parse_args()
|
|
87
|
+
|
|
88
|
+
root_package = load_json(ROOT_PACKAGE_JSON)
|
|
89
|
+
root_version = root_package.get("version")
|
|
90
|
+
if not root_version:
|
|
91
|
+
fail("root package.json is missing version")
|
|
92
|
+
|
|
93
|
+
if args.release_version and args.release_version != root_version:
|
|
94
|
+
fail(
|
|
95
|
+
f"release version {args.release_version} does not match root package.json version {root_version}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
original_payloads = {
|
|
99
|
+
CLAUDE_PLUGIN_JSON: CLAUDE_PLUGIN_JSON.read_text(),
|
|
100
|
+
CLAWHUB_SKILL_MD: CLAWHUB_SKILL_MD.read_text(),
|
|
101
|
+
OPENCLAW_PACKAGE_JSON: OPENCLAW_PACKAGE_JSON.read_text(),
|
|
102
|
+
OPENCLAW_MCP_BRIDGE: OPENCLAW_MCP_BRIDGE.read_text(),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
changed = []
|
|
106
|
+
if sync_json_version(CLAUDE_PLUGIN_JSON, root_version, "Claude plugin"):
|
|
107
|
+
changed.append(".claude-plugin/plugin.json")
|
|
108
|
+
if sync_clawhub_skill(CLAWHUB_SKILL_MD, root_version):
|
|
109
|
+
changed.append("clawhub-skill/SKILL.md")
|
|
110
|
+
if sync_json_version(OPENCLAW_PACKAGE_JSON, root_version, "OpenClaw package"):
|
|
111
|
+
changed.append("openclaw-plugin/package.json")
|
|
112
|
+
if sync_openclaw_bridge(OPENCLAW_MCP_BRIDGE, root_version):
|
|
113
|
+
changed.append("openclaw-plugin/src/mcp-bridge.ts")
|
|
114
|
+
|
|
115
|
+
if args.check:
|
|
116
|
+
for path, text in original_payloads.items():
|
|
117
|
+
path.write_text(text)
|
|
118
|
+
if changed:
|
|
119
|
+
fail("artifacts out of sync: " + ", ".join(changed))
|
|
120
|
+
print("[sync-release-artifacts] OK")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if not changed:
|
|
124
|
+
print("[sync-release-artifacts] already in sync")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
package/src/auto_update.py
CHANGED
|
@@ -17,7 +17,9 @@ import sys
|
|
|
17
17
|
import time
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
21
|
+
|
|
22
|
+
NEXO_HOME = export_resolved_nexo_home()
|
|
21
23
|
DATA_DIR = NEXO_HOME / "data"
|
|
22
24
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
25
|
|
|
@@ -148,15 +150,42 @@ def _runtime_cli_wrapper_text() -> str:
|
|
|
148
150
|
return (
|
|
149
151
|
"#!/usr/bin/env bash\n"
|
|
150
152
|
"set -euo pipefail\n\n"
|
|
151
|
-
f'
|
|
153
|
+
f'DEFAULT_NEXO_HOME="{managed_nexo_home()}"\n'
|
|
154
|
+
'RUNTIME_HOME="${NEXO_HOME:-$DEFAULT_NEXO_HOME}"\n'
|
|
155
|
+
'if [ "$RUNTIME_HOME" = "${HOME}/claude" ] && [ -e "$DEFAULT_NEXO_HOME" ]; then\n'
|
|
156
|
+
' RUNTIME_HOME="$DEFAULT_NEXO_HOME"\n'
|
|
157
|
+
'fi\n'
|
|
158
|
+
'if [ -e "$RUNTIME_HOME" ] && [ -e "$DEFAULT_NEXO_HOME" ]; then\n'
|
|
159
|
+
' RESOLVED_RUNTIME="$(cd "$RUNTIME_HOME" 2>/dev/null && pwd -P || true)"\n'
|
|
160
|
+
' RESOLVED_DEFAULT="$(cd "$DEFAULT_NEXO_HOME" 2>/dev/null && pwd -P || true)"\n'
|
|
161
|
+
' if [ -n "$RESOLVED_RUNTIME" ] && [ "$RESOLVED_RUNTIME" = "$RESOLVED_DEFAULT" ]; then\n'
|
|
162
|
+
' RUNTIME_HOME="$DEFAULT_NEXO_HOME"\n'
|
|
163
|
+
' fi\n'
|
|
164
|
+
'fi\n'
|
|
165
|
+
'NEXO_HOME="$RUNTIME_HOME"\n'
|
|
152
166
|
'export NEXO_HOME\n'
|
|
153
|
-
'
|
|
167
|
+
'resolve_code_dir() {\n'
|
|
168
|
+
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then\n'
|
|
169
|
+
' printf \'%s\\n\' "${NEXO_CODE%/}"\n'
|
|
170
|
+
' return 0\n'
|
|
171
|
+
' fi\n'
|
|
172
|
+
' if [ -f "$NEXO_HOME/cli.py" ]; then\n'
|
|
173
|
+
' printf \'%s\\n\' "$NEXO_HOME"\n'
|
|
174
|
+
' return 0\n'
|
|
175
|
+
' fi\n'
|
|
176
|
+
' printf \'%s\\n\' "$NEXO_HOME"\n'
|
|
177
|
+
'}\n'
|
|
178
|
+
'NEXO_CODE="$(resolve_code_dir)"\n'
|
|
179
|
+
'export NEXO_CODE\n'
|
|
154
180
|
'resolve_python() {\n'
|
|
155
181
|
' local candidates=()\n'
|
|
156
182
|
' local candidate=""\n'
|
|
157
183
|
' if [ -n "${NEXO_RUNTIME_PYTHON:-}" ]; then candidates+=("$NEXO_RUNTIME_PYTHON"); fi\n'
|
|
158
184
|
' if [ -n "${NEXO_PYTHON:-}" ]; then candidates+=("$NEXO_PYTHON"); fi\n'
|
|
159
|
-
' candidates+=("$
|
|
185
|
+
' candidates+=("$NEXO_CODE/.venv/bin/python3" "$NEXO_CODE/.venv/bin/python")\n'
|
|
186
|
+
' if [ "$NEXO_CODE" != "$NEXO_HOME" ]; then\n'
|
|
187
|
+
' candidates+=("$NEXO_HOME/.venv/bin/python3" "$NEXO_HOME/.venv/bin/python")\n'
|
|
188
|
+
' fi\n'
|
|
160
189
|
' case "$(uname -s)" in\n'
|
|
161
190
|
' Darwin) candidates+=("/opt/homebrew/bin/python3" "/usr/local/bin/python3") ;;\n'
|
|
162
191
|
' *) candidates+=("/usr/local/bin/python3" "/usr/bin/python3") ;;\n'
|
|
@@ -184,7 +213,17 @@ def _runtime_cli_wrapper_text() -> str:
|
|
|
184
213
|
' echo "NEXO runtime Python not found. Run nexo-brain or nexo update to repair the installation." >&2\n'
|
|
185
214
|
' exit 1\n'
|
|
186
215
|
'fi\n'
|
|
187
|
-
'
|
|
216
|
+
'CLI_PY="$NEXO_CODE/cli.py"\n'
|
|
217
|
+
'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/cli.py" ]; then\n'
|
|
218
|
+
' NEXO_CODE="$NEXO_HOME"\n'
|
|
219
|
+
' export NEXO_CODE\n'
|
|
220
|
+
' CLI_PY="$NEXO_HOME/cli.py"\n'
|
|
221
|
+
'fi\n'
|
|
222
|
+
'if [ ! -f "$CLI_PY" ]; then\n'
|
|
223
|
+
' echo "NEXO CLI not found under $NEXO_HOME. Run nexo-brain or nexo update to repair the installation." >&2\n'
|
|
224
|
+
' exit 1\n'
|
|
225
|
+
'fi\n'
|
|
226
|
+
'exec "$PYTHON" "$CLI_PY" "$@"\n'
|
|
188
227
|
)
|
|
189
228
|
|
|
190
229
|
|
|
@@ -1451,9 +1490,11 @@ def _resolve_sync_source() -> tuple[Path | None, Path | None]:
|
|
|
1451
1490
|
if (
|
|
1452
1491
|
not same_as_runtime
|
|
1453
1492
|
and (NEXO_CODE / "db").is_dir()
|
|
1454
|
-
and (NEXO_CODE.parent / "package.json").is_file()
|
|
1455
1493
|
):
|
|
1456
|
-
|
|
1494
|
+
if (NEXO_CODE.parent / "package.json").is_file():
|
|
1495
|
+
return NEXO_CODE, NEXO_CODE.parent
|
|
1496
|
+
if (NEXO_CODE / "package.json").is_file():
|
|
1497
|
+
return NEXO_CODE, NEXO_CODE
|
|
1457
1498
|
|
|
1458
1499
|
version_source = _runtime_version_source()
|
|
1459
1500
|
if version_source:
|
|
@@ -1583,7 +1624,21 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1583
1624
|
backup_dir = NEXO_HOME / "backups" / f"runtime-tree-{timestamp}"
|
|
1584
1625
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
1585
1626
|
|
|
1586
|
-
code_dirs = [
|
|
1627
|
+
code_dirs = [
|
|
1628
|
+
"hooks",
|
|
1629
|
+
"plugins",
|
|
1630
|
+
"db",
|
|
1631
|
+
"cognitive",
|
|
1632
|
+
"dashboard",
|
|
1633
|
+
"rules",
|
|
1634
|
+
"crons",
|
|
1635
|
+
"scripts",
|
|
1636
|
+
"doctor",
|
|
1637
|
+
"skills",
|
|
1638
|
+
"skills-core",
|
|
1639
|
+
"skills-runtime",
|
|
1640
|
+
"templates",
|
|
1641
|
+
]
|
|
1587
1642
|
flat_files = _runtime_flat_files(dest)
|
|
1588
1643
|
for name in code_dirs:
|
|
1589
1644
|
src = dest / name
|
|
@@ -1636,6 +1691,9 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1636
1691
|
script_conflicts: list[dict[str, str]] = []
|
|
1637
1692
|
installed_script_classes = _installed_scripts_classification(dest)
|
|
1638
1693
|
|
|
1694
|
+
for dirname in ("bin", "skills", "skills-core", "skills-runtime", "templates"):
|
|
1695
|
+
(dest / dirname).mkdir(parents=True, exist_ok=True)
|
|
1696
|
+
|
|
1639
1697
|
_emit_progress(progress_fn, "Copying core packages...")
|
|
1640
1698
|
for pkg in packages:
|
|
1641
1699
|
pkg_src = src_dir / pkg
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -16,6 +16,7 @@ from client_preferences import (
|
|
|
16
16
|
normalize_client_key,
|
|
17
17
|
normalize_client_preferences,
|
|
18
18
|
)
|
|
19
|
+
from runtime_home import resolve_nexo_home
|
|
19
20
|
|
|
20
21
|
def _resolve_templates_dir(module_file: str | os.PathLike[str]) -> Path:
|
|
21
22
|
module_dir = Path(module_file).resolve().parent
|
|
@@ -60,7 +61,7 @@ def _user_home() -> Path:
|
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
def _default_nexo_home() -> Path:
|
|
63
|
-
return
|
|
64
|
+
return resolve_nexo_home(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo")))
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
@@ -115,6 +116,18 @@ def _build_user_block(user_payload: str, template_text: str) -> str:
|
|
|
115
116
|
return f"{USER_START}\n{user_payload.rstrip()}\n{USER_END}"
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
def _extract_user_payload(text: str) -> str:
|
|
120
|
+
block = _extract_block(text, USER_START, USER_END)
|
|
121
|
+
if not block:
|
|
122
|
+
return ""
|
|
123
|
+
payload = block
|
|
124
|
+
if payload.startswith(USER_START):
|
|
125
|
+
payload = payload[len(USER_START):]
|
|
126
|
+
if payload.endswith(USER_END):
|
|
127
|
+
payload = payload[: -len(USER_END)]
|
|
128
|
+
return payload.strip("\n")
|
|
129
|
+
|
|
130
|
+
|
|
118
131
|
def _legacy_user_payload(existing_text: str) -> str:
|
|
119
132
|
cleaned = re.sub(r"<!--\s*nexo-[^-]+-[^-]+-version:\s*[\d.]+\s*-->\s*", "", existing_text)
|
|
120
133
|
if "<!-- nexo:start:" in cleaned:
|
|
@@ -146,6 +159,33 @@ def _legacy_user_payload(existing_text: str) -> str:
|
|
|
146
159
|
return cleaned.strip()
|
|
147
160
|
|
|
148
161
|
|
|
162
|
+
def _home_alias(path: Path, user_home: Path) -> str:
|
|
163
|
+
try:
|
|
164
|
+
relative = path.expanduser().relative_to(user_home.expanduser())
|
|
165
|
+
except Exception:
|
|
166
|
+
return str(path.expanduser())
|
|
167
|
+
return "~/" + relative.as_posix()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _migrate_legacy_nexo_home_references(user_payload: str, *, nexo_home: Path, user_home: Path) -> str:
|
|
171
|
+
if not user_payload.strip():
|
|
172
|
+
return user_payload
|
|
173
|
+
legacy_home = user_home.expanduser() / "claude"
|
|
174
|
+
target_home = nexo_home.expanduser()
|
|
175
|
+
if legacy_home == target_home:
|
|
176
|
+
return user_payload
|
|
177
|
+
|
|
178
|
+
replacements = [
|
|
179
|
+
(str(legacy_home), str(target_home)),
|
|
180
|
+
(_home_alias(legacy_home, user_home), _home_alias(target_home, user_home)),
|
|
181
|
+
]
|
|
182
|
+
updated = user_payload
|
|
183
|
+
for old, new in replacements:
|
|
184
|
+
if old and new and old != new:
|
|
185
|
+
updated = updated.replace(old, new)
|
|
186
|
+
return updated
|
|
187
|
+
|
|
188
|
+
|
|
149
189
|
def _target_path(client: str, *, user_home: Path | None = None) -> Path:
|
|
150
190
|
spec = BOOTSTRAP_SPECS[client]
|
|
151
191
|
home = user_home or _user_home()
|
|
@@ -243,12 +283,23 @@ def sync_client_bootstrap(
|
|
|
243
283
|
|
|
244
284
|
existing = target_path.read_text()
|
|
245
285
|
if CORE_START in existing and CORE_END in existing and USER_START in existing and USER_END in existing:
|
|
246
|
-
|
|
286
|
+
user_payload = _extract_user_payload(existing) or _extract_user_payload(rendered)
|
|
287
|
+
user_payload = _migrate_legacy_nexo_home_references(
|
|
288
|
+
user_payload,
|
|
289
|
+
nexo_home=nexo_home_path,
|
|
290
|
+
user_home=home_path,
|
|
291
|
+
)
|
|
292
|
+
user_block = _build_user_block(user_payload, rendered)
|
|
247
293
|
updated = _replace_block(existing, CORE_START, CORE_END, rendered_core)
|
|
248
294
|
updated = _replace_block(updated, USER_START, USER_END, user_block)
|
|
249
295
|
action = "updated" if updated != existing else "unchanged"
|
|
250
296
|
else:
|
|
251
297
|
legacy_user = _legacy_user_payload(existing)
|
|
298
|
+
legacy_user = _migrate_legacy_nexo_home_references(
|
|
299
|
+
legacy_user,
|
|
300
|
+
nexo_home=nexo_home_path,
|
|
301
|
+
user_home=home_path,
|
|
302
|
+
)
|
|
252
303
|
updated = _replace_block(rendered, USER_START, USER_END, _build_user_block(legacy_user, rendered))
|
|
253
304
|
action = "migrated"
|
|
254
305
|
|