nexo-brain 5.3.0 → 5.3.2
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 +10 -1
- package/bin/nexo-brain.js +59 -4
- package/bin/nexo.js +43 -7
- package/hooks/hooks.json +17 -0
- package/package.json +2 -1
- package/scripts/sync_release_artifacts.py +128 -0
- package/src/auto_update.py +70 -8
- package/src/bootstrap_docs.py +53 -2
- package/src/cli.py +3 -1
- package/src/client_sync.py +14 -1
- package/src/db/_personal_scripts.py +30 -8
- package/src/doctor/providers/runtime.py +37 -1
- package/src/hooks/heartbeat-enforcement.py +90 -0
- package/src/hooks/heartbeat-posttool.sh +18 -0
- package/src/hooks/heartbeat-user-msg.sh +15 -0
- package/src/hooks/session-start.sh +8 -4
- package/src/plugins/update.py +134 -4
- package/src/runtime_home.py +46 -0
- package/src/script_registry.py +47 -7
- 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/launchagents/README.md +0 -1
- package/templates/nexo_helper.py +5 -1
- package/templates/launchagents/com.nexo.github-monitor.plist +0 -45
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.2",
|
|
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,9 +87,18 @@ 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.2` hardens the packaged runtime boundary: NEXO now persists which runtime scripts/hooks are core product artifacts, `nexo scripts` no longer mixes those into the personal bucket, and `nexo update` migrates the legacy Claude Code heartbeat wrappers into managed core hooks.
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
90
94
|
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.
|
|
91
95
|
|
|
92
|
-
Version `5.2.1`
|
|
96
|
+
Version `5.2.1` fixes the Deep Sleep datetime regression and closes the decision-to-outcome feedback gap:
|
|
97
|
+
|
|
98
|
+
- `_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.
|
|
99
|
+
- `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.
|
|
100
|
+
|
|
101
|
+
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:
|
|
93
102
|
|
|
94
103
|
- `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.
|
|
95
104
|
- `NEGATION_PATTERNS` suppresses false positives when the user explicitly disclaims touching the sensitive area (`sin afectar producción`, `no tocar prod`, `without touching production`, `don't modify`). The raw keyword being present is no longer enough to flag the task.
|
package/bin/nexo-brain.js
CHANGED
|
@@ -93,6 +93,33 @@ function syncWatchdogHashRegistry(nexoHome) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function writeRuntimeCoreArtifactsManifest(nexoHome, srcDir) {
|
|
97
|
+
try {
|
|
98
|
+
const listTopLevelFiles = (dirPath) => {
|
|
99
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
100
|
+
return fs.readdirSync(dirPath)
|
|
101
|
+
.filter((name) => {
|
|
102
|
+
const full = path.join(dirPath, name);
|
|
103
|
+
return fs.existsSync(full) && fs.statSync(full).isFile();
|
|
104
|
+
})
|
|
105
|
+
.sort();
|
|
106
|
+
};
|
|
107
|
+
const configDir = path.join(nexoHome, "config");
|
|
108
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
109
|
+
const payload = {
|
|
110
|
+
generated_at: new Date().toISOString(),
|
|
111
|
+
script_names: listTopLevelFiles(path.join(srcDir, "scripts")),
|
|
112
|
+
hook_names: listTopLevelFiles(path.join(srcDir, "hooks")),
|
|
113
|
+
};
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(configDir, "runtime-core-artifacts.json"),
|
|
116
|
+
`${JSON.stringify(payload, null, 2)}\n`
|
|
117
|
+
);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
log(`WARN: could not write runtime core-artifacts manifest: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
function getCoreRuntimeFlatFiles() {
|
|
97
124
|
return [
|
|
98
125
|
"server.py",
|
|
@@ -1538,6 +1565,7 @@ async function main() {
|
|
|
1538
1565
|
fs.chmodSync(path.join(scriptsDest, f), "755");
|
|
1539
1566
|
});
|
|
1540
1567
|
}
|
|
1568
|
+
writeRuntimeCoreArtifactsManifest(NEXO_HOME, srcDir);
|
|
1541
1569
|
log(" Scripts updated.");
|
|
1542
1570
|
|
|
1543
1571
|
// Register ALL 8 core hooks in settings.json (additive — don't remove user's custom hooks)
|
|
@@ -2210,15 +2238,31 @@ async function main() {
|
|
|
2210
2238
|
"#!/usr/bin/env bash",
|
|
2211
2239
|
"set -euo pipefail",
|
|
2212
2240
|
"",
|
|
2213
|
-
`
|
|
2241
|
+
`RUNTIME_HOME="${NEXO_HOME}"`,
|
|
2242
|
+
'NEXO_HOME="$RUNTIME_HOME"',
|
|
2214
2243
|
'export NEXO_HOME',
|
|
2215
|
-
'
|
|
2244
|
+
'resolve_code_dir() {',
|
|
2245
|
+
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then',
|
|
2246
|
+
' printf \'%s\\n\' "${NEXO_CODE%/}"',
|
|
2247
|
+
' return 0',
|
|
2248
|
+
' fi',
|
|
2249
|
+
' if [ -f "$NEXO_HOME/cli.py" ]; then',
|
|
2250
|
+
' printf \'%s\\n\' "$NEXO_HOME"',
|
|
2251
|
+
' return 0',
|
|
2252
|
+
' fi',
|
|
2253
|
+
' printf \'%s\\n\' "$NEXO_HOME"',
|
|
2254
|
+
'}',
|
|
2255
|
+
'NEXO_CODE="$(resolve_code_dir)"',
|
|
2256
|
+
'export NEXO_CODE',
|
|
2216
2257
|
'resolve_python() {',
|
|
2217
2258
|
' local candidates=()',
|
|
2218
2259
|
' local candidate=""',
|
|
2219
2260
|
' if [ -n "${NEXO_RUNTIME_PYTHON:-}" ]; then candidates+=("$NEXO_RUNTIME_PYTHON"); fi',
|
|
2220
2261
|
' if [ -n "${NEXO_PYTHON:-}" ]; then candidates+=("$NEXO_PYTHON"); fi',
|
|
2221
|
-
' candidates+=("$
|
|
2262
|
+
' candidates+=("$NEXO_CODE/.venv/bin/python3" "$NEXO_CODE/.venv/bin/python")',
|
|
2263
|
+
' if [ "$NEXO_CODE" != "$NEXO_HOME" ]; then',
|
|
2264
|
+
' candidates+=("$NEXO_HOME/.venv/bin/python3" "$NEXO_HOME/.venv/bin/python")',
|
|
2265
|
+
' fi',
|
|
2222
2266
|
' case "$(uname -s)" in',
|
|
2223
2267
|
' Darwin) candidates+=("/opt/homebrew/bin/python3" "/usr/local/bin/python3") ;;',
|
|
2224
2268
|
' *) candidates+=("/usr/local/bin/python3" "/usr/bin/python3") ;;',
|
|
@@ -2246,7 +2290,17 @@ async function main() {
|
|
|
2246
2290
|
' echo "NEXO runtime Python not found. Run nexo-brain or nexo update to repair the installation." >&2',
|
|
2247
2291
|
' exit 1',
|
|
2248
2292
|
'fi',
|
|
2249
|
-
'
|
|
2293
|
+
'CLI_PY="$NEXO_CODE/cli.py"',
|
|
2294
|
+
'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/cli.py" ]; then',
|
|
2295
|
+
' NEXO_CODE="$NEXO_HOME"',
|
|
2296
|
+
' export NEXO_CODE',
|
|
2297
|
+
' CLI_PY="$NEXO_HOME/cli.py"',
|
|
2298
|
+
'fi',
|
|
2299
|
+
'if [ ! -f "$CLI_PY" ]; then',
|
|
2300
|
+
' echo "NEXO CLI not found under $NEXO_HOME. Run nexo-brain or nexo update to repair the installation." >&2',
|
|
2301
|
+
' exit 1',
|
|
2302
|
+
'fi',
|
|
2303
|
+
'exec "$PYTHON" "$CLI_PY" "$@"',
|
|
2250
2304
|
"",
|
|
2251
2305
|
].join("\n");
|
|
2252
2306
|
const runtimeCliPath = path.join(NEXO_HOME, "bin", "nexo");
|
|
@@ -2333,6 +2387,7 @@ async function main() {
|
|
|
2333
2387
|
});
|
|
2334
2388
|
log(" Hooks installed.");
|
|
2335
2389
|
}
|
|
2390
|
+
writeRuntimeCoreArtifactsManifest(NEXO_HOME, srcDir);
|
|
2336
2391
|
|
|
2337
2392
|
// Generate personality
|
|
2338
2393
|
const personality = `# ${operatorName} — Personality
|
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/hooks/hooks.json
CHANGED
|
@@ -50,6 +50,18 @@
|
|
|
50
50
|
]
|
|
51
51
|
}
|
|
52
52
|
],
|
|
53
|
+
"UserPromptSubmit": [
|
|
54
|
+
{
|
|
55
|
+
"matcher": "*",
|
|
56
|
+
"hooks": [
|
|
57
|
+
{
|
|
58
|
+
"type": "command",
|
|
59
|
+
"command": "NEXO_HOME=\"${CLAUDE_PLUGIN_DATA}\" NEXO_CODE=\"${CLAUDE_PLUGIN_ROOT}/src\" bash \"${CLAUDE_PLUGIN_ROOT}/src/hooks/heartbeat-user-msg.sh\"",
|
|
60
|
+
"timeout": 3
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
53
65
|
"PostToolUse": [
|
|
54
66
|
{
|
|
55
67
|
"matcher": "*",
|
|
@@ -73,6 +85,11 @@
|
|
|
73
85
|
"type": "command",
|
|
74
86
|
"command": "NEXO_HOME=\"${CLAUDE_PLUGIN_DATA}\" NEXO_CODE=\"${CLAUDE_PLUGIN_ROOT}/src\" bash \"${CLAUDE_PLUGIN_ROOT}/src/hooks/protocol-guardrail.sh\"",
|
|
75
87
|
"timeout": 5
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "NEXO_HOME=\"${CLAUDE_PLUGIN_DATA}\" NEXO_CODE=\"${CLAUDE_PLUGIN_ROOT}/src\" bash \"${CLAUDE_PLUGIN_ROOT}/src/hooks/heartbeat-posttool.sh\"",
|
|
92
|
+
"timeout": 3
|
|
76
93
|
}
|
|
77
94
|
]
|
|
78
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.2",
|
|
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
|
|
|
@@ -320,6 +359,10 @@ def _cleanup_retired_runtime_files():
|
|
|
320
359
|
"""Remove retired core files that should not survive updates."""
|
|
321
360
|
retired = [
|
|
322
361
|
NEXO_HOME / "scripts" / "nexo-day-orchestrator.sh",
|
|
362
|
+
NEXO_HOME / "scripts" / "heartbeat-enforcement.py",
|
|
363
|
+
NEXO_HOME / "scripts" / "heartbeat-posttool.sh",
|
|
364
|
+
NEXO_HOME / "scripts" / "heartbeat-user-msg.sh",
|
|
365
|
+
NEXO_HOME / "hooks" / "heartbeat-guard.sh",
|
|
323
366
|
]
|
|
324
367
|
for target in retired:
|
|
325
368
|
try:
|
|
@@ -1451,9 +1494,11 @@ def _resolve_sync_source() -> tuple[Path | None, Path | None]:
|
|
|
1451
1494
|
if (
|
|
1452
1495
|
not same_as_runtime
|
|
1453
1496
|
and (NEXO_CODE / "db").is_dir()
|
|
1454
|
-
and (NEXO_CODE.parent / "package.json").is_file()
|
|
1455
1497
|
):
|
|
1456
|
-
|
|
1498
|
+
if (NEXO_CODE.parent / "package.json").is_file():
|
|
1499
|
+
return NEXO_CODE, NEXO_CODE.parent
|
|
1500
|
+
if (NEXO_CODE / "package.json").is_file():
|
|
1501
|
+
return NEXO_CODE, NEXO_CODE
|
|
1457
1502
|
|
|
1458
1503
|
version_source = _runtime_version_source()
|
|
1459
1504
|
if version_source:
|
|
@@ -1583,7 +1628,21 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1583
1628
|
backup_dir = NEXO_HOME / "backups" / f"runtime-tree-{timestamp}"
|
|
1584
1629
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
1585
1630
|
|
|
1586
|
-
code_dirs = [
|
|
1631
|
+
code_dirs = [
|
|
1632
|
+
"hooks",
|
|
1633
|
+
"plugins",
|
|
1634
|
+
"db",
|
|
1635
|
+
"cognitive",
|
|
1636
|
+
"dashboard",
|
|
1637
|
+
"rules",
|
|
1638
|
+
"crons",
|
|
1639
|
+
"scripts",
|
|
1640
|
+
"doctor",
|
|
1641
|
+
"skills",
|
|
1642
|
+
"skills-core",
|
|
1643
|
+
"skills-runtime",
|
|
1644
|
+
"templates",
|
|
1645
|
+
]
|
|
1587
1646
|
flat_files = _runtime_flat_files(dest)
|
|
1588
1647
|
for name in code_dirs:
|
|
1589
1648
|
src = dest / name
|
|
@@ -1636,6 +1695,9 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1636
1695
|
script_conflicts: list[dict[str, str]] = []
|
|
1637
1696
|
installed_script_classes = _installed_scripts_classification(dest)
|
|
1638
1697
|
|
|
1698
|
+
for dirname in ("bin", "skills", "skills-core", "skills-runtime", "templates"):
|
|
1699
|
+
(dest / dirname).mkdir(parents=True, exist_ok=True)
|
|
1700
|
+
|
|
1639
1701
|
_emit_progress(progress_fn, "Copying core packages...")
|
|
1640
1702
|
for pkg in packages:
|
|
1641
1703
|
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
|
|
package/src/cli.py
CHANGED
|
@@ -40,7 +40,9 @@ import subprocess
|
|
|
40
40
|
import sys
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
from runtime_home import export_resolved_nexo_home
|
|
44
|
+
|
|
45
|
+
NEXO_HOME = export_resolved_nexo_home()
|
|
44
46
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
45
47
|
TERMINAL_CLIENT_LABELS = {
|
|
46
48
|
"claude_code": "Claude Code",
|