nexo-brain 5.3.26 → 5.3.28
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/package.json +1 -1
- package/src/hook_guardrails.py +44 -0
- package/src/server.py +3 -0
- package/src/tools_sessions.py +6 -1
- package/src/dashboard/static/favicon 2.svg +0 -32
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +0 -40
- package/src/dashboard/static/style 2.css +0 -2458
- package/src/dashboard/templates/adaptive 2.html +0 -118
- package/src/dashboard/templates/artifacts 2.html +0 -133
- package/src/dashboard/templates/backups 2.html +0 -136
- package/src/dashboard/templates/base 2.html +0 -417
- package/src/dashboard/templates/calendar 2.html +0 -591
- package/src/dashboard/templates/chat 2.html +0 -356
- package/src/dashboard/templates/claims 2.html +0 -259
- package/src/dashboard/templates/cortex 2.html +0 -321
- package/src/dashboard/templates/credentials 2.html +0 -128
- package/src/dashboard/templates/crons 2.html +0 -370
- package/src/dashboard/templates/dashboard 2.html +0 -494
- package/src/dashboard/templates/dreams 2.html +0 -252
- package/src/dashboard/templates/email 2.html +0 -160
- package/src/dashboard/templates/evolution 2.html +0 -189
- package/src/dashboard/templates/feed 2.html +0 -249
- package/src/dashboard/templates/followup_health 2.html +0 -170
- package/src/dashboard/templates/graph 2.html +0 -201
- package/src/dashboard/templates/guard 2.html +0 -259
- package/src/dashboard/templates/inbox 2.html +0 -251
- package/src/dashboard/templates/memory 2.html +0 -420
- package/src/dashboard/templates/operations 2.html +0 -608
- package/src/dashboard/templates/plugins 2.html +0 -185
- package/src/dashboard/templates/protocol 2.html +0 -199
- package/src/dashboard/templates/rules 2.html +0 -246
- package/src/dashboard/templates/sentiment 2.html +0 -247
- package/src/dashboard/templates/sessions 2.html +0 -218
- package/src/dashboard/templates/skills 2.html +0 -329
- package/src/dashboard/templates/somatic 2.html +0 -73
- package/src/dashboard/templates/triggers 2.html +0 -133
- package/src/dashboard/templates/trust 2.html +0 -360
- package/src/db/__init__ 2.py +0 -259
- package/src/db/_core 2.py +0 -437
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_episodic 2.py +0 -762
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_goal_profiles 2.py +0 -376
- package/src/db/_hot_context 2.py +0 -660
- package/src/db/_outcomes 2.py +0 -800
- package/src/db/_personal_scripts 2.py +0 -582
- package/src/db/_sessions 2.py +0 -330
- package/src/db/_tasks 2.py +0 -91
- package/src/db/_watchers 2.py +0 -173
- package/src/doctor/formatters 2.py +0 -52
- package/src/doctor/models 2.py +0 -69
- package/src/doctor/planes 2.py +0 -87
- package/src/doctor/providers/__init__ 2.py +0 -1
- package/src/doctor/providers/deep 2.py +0 -367
- package/src/evolution_cycle 2.py +0 -519
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -158
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/heartbeat-enforcement 2.py +0 -90
- package/src/hooks/heartbeat-posttool 2.sh +0 -18
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -152
- package/src/hooks/pre-compact 2.sh +0 -169
- package/src/hooks/protocol-guardrail 2.sh +0 -10
- package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
- package/src/hooks/session-stop 2.sh +0 -52
- package/src/kg_populate 2.py +0 -292
- package/src/maintenance 2.py +0 -53
- package/src/memory_backends 2.py +0 -71
- package/src/migrate_embeddings 2.py +0 -124
- package/src/nexo_sdk 2.py +0 -103
- package/src/observability 2.py +0 -199
- package/src/plugin_loader 2.py +0 -217
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -127
- package/src/plugins/claims_tools 2.py +0 -119
- package/src/plugins/cognitive_memory 2.py +0 -609
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -1155
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -560
- package/src/plugins/evolution 2.py +0 -167
- package/src/plugins/goal_engine 2.py +0 -142
- package/src/plugins/guard 2.py +0 -862
- package/src/plugins/impact 2.py +0 -29
- package/src/plugins/knowledge_graph_tools 2.py +0 -137
- package/src/plugins/media_memory_tools 2.py +0 -98
- package/src/plugins/memory_export 2.py +0 -196
- package/src/plugins/outcomes 2.py +0 -130
- package/src/plugins/personal_scripts 2.py +0 -117
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/protocol 2.py +0 -1449
- package/src/plugins/simple_api 2.py +0 -106
- package/src/plugins/skills 2.py +0 -341
- package/src/plugins/state_watchers 2.py +0 -79
- package/src/plugins/update 2.py +0 -986
- package/src/plugins/user_state_tools 2.py +0 -43
- package/src/plugins/workflow 2.py +0 -588
- package/src/protocol_settings 2.py +0 -59
- package/src/public_contribution 2.py +0 -466
- package/src/public_evolution_queue 2.py +0 -241
- package/src/requirements 2.txt +0 -14
- package/src/retroactive_learnings 2.py +0 -373
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/runtime_power 2.py +0 -874
- package/src/script_registry 2.py +0 -1559
- package/src/scripts/check-context 2.py +0 -272
- package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
- package/src/scripts/deep-sleep/collect 2.py +0 -928
- package/src/scripts/deep-sleep/extract 2.py +0 -330
- package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
- package/src/scripts/deep-sleep/synthesize 2.py +0 -312
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
- package/src/scripts/nexo-agent-run 2.py +0 -75
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -300
- package/src/scripts/nexo-cognitive-decay 2.py +0 -257
- package/src/scripts/nexo-cortex-cycle 2.py +0 -293
- package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
- package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
- package/src/scripts/nexo-dashboard 2.sh +0 -29
- package/src/scripts/nexo-deep-sleep 2.sh +0 -86
- package/src/scripts/nexo-evolution-run 2.py +0 -1664
- package/src/scripts/nexo-followup-hygiene 2.py +0 -139
- package/src/scripts/nexo-hook-record 2.py +0 -42
- package/src/scripts/nexo-immune 2.py +0 -936
- package/src/scripts/nexo-impact-scorer 2.py +0 -117
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -401
- package/src/scripts/nexo-learning-validator 2.py +0 -266
- package/src/scripts/nexo-migrate 2.py +0 -260
- package/src/scripts/nexo-outcome-checker 2.py +0 -127
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
- package/src/scripts/nexo-reflection 2.py +0 -256
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-sleep 2.py +0 -631
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-sync-clients 2.py +0 -16
- package/src/scripts/nexo-synthesis 2.py +0 -475
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -306
- package/src/scripts/nexo-watchdog 2.sh +0 -1207
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
- package/src/server 2.py +0 -1296
- package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
- package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
- package/src/skills/run-release-final-audit/guide 2.md +0 -16
- package/src/skills/run-release-final-audit/script 2.py +0 -259
- package/src/skills/run-release-final-audit/skill 2.json +0 -77
- package/src/skills/run-runtime-doctor/guide 2.md +0 -12
- package/src/skills/run-runtime-doctor/script 2.py +0 -21
- package/src/skills/run-runtime-doctor/skill 2.json +0 -25
- package/src/skills_runtime 2.py +0 -932
- package/src/state_watchers_runtime 2.py +0 -475
- package/src/storage_router 2.py +0 -32
- package/src/system_catalog 2.py +0 -786
- package/src/tools_coordination 2.py +0 -103
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_drive 2.py +0 -487
- package/src/tools_hot_context 2.py +0 -163
- package/src/tools_learnings 2.py +0 -612
- package/src/tools_menu 2.py +0 -229
- package/src/tools_reminders 2.py +0 -88
- package/src/tools_reminders_crud 2.py +0 -363
- package/src/tools_sessions 2.py +0 -1054
- package/src/tools_system_catalog 2.py +0 -19
- package/src/tools_task_history 2.py +0 -57
- package/src/tools_transcripts 2.py +0 -98
- package/src/transcript_utils 2.py +0 -412
- package/src/user_context 2.py +0 -46
- package/src/user_data_portability 2.py +0 -328
- package/src/user_state_model 2.py +0 -170
- package/templates/CLAUDE.md 2.template +0 -108
- package/templates/CODEX.AGENTS.md 2.template +0 -66
- package/templates/launchagents/README 2.md +0 -132
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
- package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
- package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
- package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
- package/templates/launchagents/com.nexo.immune 2.plist +0 -41
- package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
- package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
- package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
- package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
- package/templates/nexo_helper 2.py +0 -301
- package/templates/openclaw 2.json +0 -13
- package/templates/plugin-template 2.py +0 -40
- package/templates/script-template 2.py +0 -59
- package/templates/script-template 2.sh +0 -13
- package/templates/skill-script-template 2.py +0 -48
- package/templates/skill-template 2.md +0 -33
package/src/plugins/update 2.py
DELETED
|
@@ -1,986 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
"""Update plugin — pull latest code, backup DBs, run migrations, verify."""
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import shutil
|
|
6
|
-
import sqlite3
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
|
-
import time
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from runtime_home import export_resolved_nexo_home
|
|
13
|
-
|
|
14
|
-
# Code root is the parent of plugins/:
|
|
15
|
-
# - source checkout: <repo>/src
|
|
16
|
-
# - packaged runtime: <NEXO_HOME>
|
|
17
|
-
_THIS_DIR = Path(__file__).resolve().parent
|
|
18
|
-
CODE_ROOT = _THIS_DIR.parent
|
|
19
|
-
_REPO_CANDIDATE = CODE_ROOT.parent
|
|
20
|
-
|
|
21
|
-
NEXO_HOME = export_resolved_nexo_home()
|
|
22
|
-
DATA_DIR = NEXO_HOME / "data"
|
|
23
|
-
BACKUP_BASE = NEXO_HOME / "backups"
|
|
24
|
-
|
|
25
|
-
# In packaged installs, update.py lives at <NEXO_HOME>/plugins/update.py.
|
|
26
|
-
_PACKAGED_INSTALL = not (_REPO_CANDIDATE / ".git").exists() and not (_REPO_CANDIDATE / ".git").is_file()
|
|
27
|
-
REPO_DIR = CODE_ROOT if _PACKAGED_INSTALL else _REPO_CANDIDATE
|
|
28
|
-
SRC_DIR = CODE_ROOT
|
|
29
|
-
PACKAGE_JSON = REPO_DIR / "package.json"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _venv_python_path(runtime_root: Path = NEXO_HOME) -> Path:
|
|
33
|
-
if sys.platform == "win32":
|
|
34
|
-
return runtime_root / ".venv" / "Scripts" / "python.exe"
|
|
35
|
-
return runtime_root / ".venv" / "bin" / "python3"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _venv_pip_path(runtime_root: Path = NEXO_HOME) -> Path:
|
|
39
|
-
if sys.platform == "win32":
|
|
40
|
-
return runtime_root / ".venv" / "Scripts" / "pip.exe"
|
|
41
|
-
return runtime_root / ".venv" / "bin" / "pip"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _ensure_managed_venv(runtime_root: Path = NEXO_HOME) -> str | None:
|
|
45
|
-
venv_python = _venv_python_path(runtime_root)
|
|
46
|
-
if venv_python.exists():
|
|
47
|
-
return None
|
|
48
|
-
try:
|
|
49
|
-
runtime_root.mkdir(parents=True, exist_ok=True)
|
|
50
|
-
result = subprocess.run(
|
|
51
|
-
[sys.executable, "-m", "venv", str(runtime_root / ".venv")],
|
|
52
|
-
capture_output=True,
|
|
53
|
-
text=True,
|
|
54
|
-
timeout=120,
|
|
55
|
-
)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
return f"venv creation error: {e}"
|
|
58
|
-
if result.returncode != 0 or not venv_python.exists():
|
|
59
|
-
return f"venv creation failed: {result.stderr or result.stdout}"
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _find_npm_pkg_src() -> Path | None:
|
|
64
|
-
"""Locate the nexo-brain npm package's src/ directory for requirements.txt."""
|
|
65
|
-
try:
|
|
66
|
-
result = subprocess.run(
|
|
67
|
-
["npm", "root", "-g"],
|
|
68
|
-
capture_output=True, text=True, timeout=10,
|
|
69
|
-
)
|
|
70
|
-
if result.returncode == 0:
|
|
71
|
-
npm_src = Path(result.stdout.strip()) / "nexo-brain" / "src"
|
|
72
|
-
if npm_src.is_dir():
|
|
73
|
-
return npm_src
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _core_artifact_source_dir() -> Path | None:
|
|
80
|
-
"""Return the canonical source directory for packaged core artifacts."""
|
|
81
|
-
if _PACKAGED_INSTALL:
|
|
82
|
-
return _find_npm_pkg_src()
|
|
83
|
-
return SRC_DIR
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _is_git_repo() -> bool:
|
|
87
|
-
"""Check if REPO_DIR is a valid git repository."""
|
|
88
|
-
return (REPO_DIR / ".git").exists() or (REPO_DIR / ".git").is_file()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _refresh_installed_manifest():
|
|
92
|
-
"""Refresh packaged crons and persist the runtime core-artifacts manifest."""
|
|
93
|
-
try:
|
|
94
|
-
artifact_src = _core_artifact_source_dir()
|
|
95
|
-
if artifact_src is None:
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
src_crons = artifact_src / "crons"
|
|
99
|
-
dst_crons = NEXO_HOME / "crons"
|
|
100
|
-
if src_crons.exists():
|
|
101
|
-
dst_crons.mkdir(parents=True, exist_ok=True)
|
|
102
|
-
for f in src_crons.iterdir():
|
|
103
|
-
if f.is_file():
|
|
104
|
-
dest = dst_crons / f.name
|
|
105
|
-
if _paths_match(f, dest):
|
|
106
|
-
continue
|
|
107
|
-
shutil.copy2(str(f), str(dest))
|
|
108
|
-
config_dir = NEXO_HOME / "config"
|
|
109
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
-
payload = {
|
|
111
|
-
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
112
|
-
"script_names": sorted(
|
|
113
|
-
f.name for f in (artifact_src / "scripts").iterdir()
|
|
114
|
-
if f.is_file()
|
|
115
|
-
) if (artifact_src / "scripts").is_dir() else [],
|
|
116
|
-
"hook_names": sorted(
|
|
117
|
-
f.name for f in (artifact_src / "hooks").iterdir()
|
|
118
|
-
if f.is_file()
|
|
119
|
-
) if (artifact_src / "hooks").is_dir() else [],
|
|
120
|
-
}
|
|
121
|
-
(config_dir / "runtime-core-artifacts.json").write_text(
|
|
122
|
-
json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
|
123
|
-
)
|
|
124
|
-
except Exception:
|
|
125
|
-
pass
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _cleanup_retired_runtime_files() -> list[str]:
|
|
129
|
-
removed: list[str] = []
|
|
130
|
-
retired_paths = [
|
|
131
|
-
NEXO_HOME / "scripts" / "heartbeat-enforcement.py",
|
|
132
|
-
NEXO_HOME / "scripts" / "heartbeat-posttool.sh",
|
|
133
|
-
NEXO_HOME / "scripts" / "heartbeat-user-msg.sh",
|
|
134
|
-
NEXO_HOME / "hooks" / "heartbeat-guard.sh",
|
|
135
|
-
]
|
|
136
|
-
for path in retired_paths:
|
|
137
|
-
if not path.exists():
|
|
138
|
-
continue
|
|
139
|
-
try:
|
|
140
|
-
path.unlink()
|
|
141
|
-
removed.append(str(path))
|
|
142
|
-
except Exception:
|
|
143
|
-
continue
|
|
144
|
-
return removed
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _read_version() -> str:
|
|
148
|
-
"""Read the installed/runtime version."""
|
|
149
|
-
if _PACKAGED_INSTALL:
|
|
150
|
-
# version.json is the runtime truth for packaged installs.
|
|
151
|
-
try:
|
|
152
|
-
version_file = NEXO_HOME / "version.json"
|
|
153
|
-
if version_file.exists():
|
|
154
|
-
return json.loads(version_file.read_text()).get("version", "unknown")
|
|
155
|
-
except Exception:
|
|
156
|
-
pass
|
|
157
|
-
try:
|
|
158
|
-
package_file = NEXO_HOME / "package.json"
|
|
159
|
-
if package_file.exists():
|
|
160
|
-
return json.loads(package_file.read_text()).get("version", "unknown")
|
|
161
|
-
except Exception:
|
|
162
|
-
pass
|
|
163
|
-
|
|
164
|
-
try:
|
|
165
|
-
if PACKAGE_JSON.exists():
|
|
166
|
-
return json.loads(PACKAGE_JSON.read_text()).get("version", "unknown")
|
|
167
|
-
except Exception:
|
|
168
|
-
pass
|
|
169
|
-
try:
|
|
170
|
-
version_file = NEXO_HOME / "version.json"
|
|
171
|
-
if version_file.exists():
|
|
172
|
-
return json.loads(version_file.read_text()).get("version", "unknown")
|
|
173
|
-
except Exception:
|
|
174
|
-
pass
|
|
175
|
-
return "unknown"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _git(*args, cwd=None) -> tuple[int, str, str]:
|
|
179
|
-
"""Run a git command and return (returncode, stdout, stderr)."""
|
|
180
|
-
result = subprocess.run(
|
|
181
|
-
["git"] + list(args),
|
|
182
|
-
cwd=cwd or str(REPO_DIR),
|
|
183
|
-
capture_output=True,
|
|
184
|
-
text=True,
|
|
185
|
-
timeout=60,
|
|
186
|
-
)
|
|
187
|
-
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def _requirements_hash() -> str:
|
|
191
|
-
"""Return a content hash of requirements.txt, or empty string if missing."""
|
|
192
|
-
import hashlib
|
|
193
|
-
req_file = SRC_DIR / "requirements.txt"
|
|
194
|
-
if not req_file.exists() and _PACKAGED_INSTALL:
|
|
195
|
-
npm_src = _find_npm_pkg_src()
|
|
196
|
-
if npm_src:
|
|
197
|
-
req_file = npm_src / "requirements.txt"
|
|
198
|
-
if req_file.exists():
|
|
199
|
-
return hashlib.sha256(req_file.read_bytes()).hexdigest()
|
|
200
|
-
return ""
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _check_dirty() -> str | None:
|
|
204
|
-
"""Return error message if worktree has uncommitted changes, else None."""
|
|
205
|
-
if not _is_git_repo():
|
|
206
|
-
return None # Not a git repo, skip dirty check
|
|
207
|
-
rc, out, _ = _git("status", "--porcelain")
|
|
208
|
-
if rc != 0:
|
|
209
|
-
return "Failed to check git status."
|
|
210
|
-
if out:
|
|
211
|
-
return f"Uncommitted changes:\n{out}\nCommit or stash before updating."
|
|
212
|
-
return None
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _backup_databases() -> tuple[str, str | None]:
|
|
216
|
-
"""Backup all .db files from NEXO_HOME/data/. Returns (backup_dir, error)."""
|
|
217
|
-
timestamp = time.strftime("%Y-%m-%d-%H%M")
|
|
218
|
-
backup_dir = BACKUP_BASE / f"pre-update-{timestamp}"
|
|
219
|
-
|
|
220
|
-
db_files = list(DATA_DIR.glob("*.db")) if DATA_DIR.is_dir() else []
|
|
221
|
-
# Also check NEXO_HOME root for legacy db location
|
|
222
|
-
db_files += [f for f in NEXO_HOME.glob("*.db") if f.is_file()]
|
|
223
|
-
# And check src/ dir for nexo.db (dev mode)
|
|
224
|
-
src_db = SRC_DIR / "nexo.db"
|
|
225
|
-
if src_db.is_file() and src_db not in db_files:
|
|
226
|
-
db_files.append(src_db)
|
|
227
|
-
|
|
228
|
-
if not db_files:
|
|
229
|
-
return str(backup_dir), None # No DBs to backup, not an error
|
|
230
|
-
|
|
231
|
-
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
-
|
|
233
|
-
for db_file in db_files:
|
|
234
|
-
dest = backup_dir / db_file.name
|
|
235
|
-
src_conn = None
|
|
236
|
-
dst_conn = None
|
|
237
|
-
try:
|
|
238
|
-
src_conn = sqlite3.connect(str(db_file))
|
|
239
|
-
dst_conn = sqlite3.connect(str(dest))
|
|
240
|
-
src_conn.backup(dst_conn)
|
|
241
|
-
except Exception as e:
|
|
242
|
-
return str(backup_dir), f"Failed to backup {db_file.name}: {e}"
|
|
243
|
-
finally:
|
|
244
|
-
for conn in (dst_conn, src_conn):
|
|
245
|
-
if conn is not None:
|
|
246
|
-
try:
|
|
247
|
-
conn.close()
|
|
248
|
-
except Exception:
|
|
249
|
-
pass
|
|
250
|
-
|
|
251
|
-
return str(backup_dir), None
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def _restore_databases(backup_dir: str):
|
|
255
|
-
"""Restore .db files from a backup directory."""
|
|
256
|
-
bdir = Path(backup_dir)
|
|
257
|
-
if not bdir.is_dir():
|
|
258
|
-
return
|
|
259
|
-
for db_backup in bdir.glob("*.db"):
|
|
260
|
-
# Try to find original location
|
|
261
|
-
for candidate in [DATA_DIR / db_backup.name, NEXO_HOME / db_backup.name, SRC_DIR / db_backup.name]:
|
|
262
|
-
if candidate.is_file():
|
|
263
|
-
src_conn = None
|
|
264
|
-
dst_conn = None
|
|
265
|
-
try:
|
|
266
|
-
src_conn = sqlite3.connect(str(db_backup))
|
|
267
|
-
dst_conn = sqlite3.connect(str(candidate))
|
|
268
|
-
src_conn.backup(dst_conn)
|
|
269
|
-
except Exception:
|
|
270
|
-
pass
|
|
271
|
-
finally:
|
|
272
|
-
for conn in (dst_conn, src_conn):
|
|
273
|
-
if conn is not None:
|
|
274
|
-
try:
|
|
275
|
-
conn.close()
|
|
276
|
-
except Exception:
|
|
277
|
-
pass
|
|
278
|
-
break
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def _reinstall_pip_deps() -> str | None:
|
|
282
|
-
"""Reinstall Python dependencies from requirements.txt into the managed venv."""
|
|
283
|
-
req_file = SRC_DIR / "requirements.txt"
|
|
284
|
-
if not req_file.exists() and _PACKAGED_INSTALL:
|
|
285
|
-
# In packaged mode, requirements.txt lives in the npm package's src/ dir
|
|
286
|
-
npm_src = _find_npm_pkg_src()
|
|
287
|
-
if npm_src:
|
|
288
|
-
req_file = npm_src / "requirements.txt"
|
|
289
|
-
if not req_file.exists():
|
|
290
|
-
return None # No requirements file, skip
|
|
291
|
-
venv_error = _ensure_managed_venv(NEXO_HOME)
|
|
292
|
-
if venv_error is not None:
|
|
293
|
-
return venv_error
|
|
294
|
-
venv_pip = _venv_pip_path(NEXO_HOME)
|
|
295
|
-
if not venv_pip.exists() and sys.platform != "win32":
|
|
296
|
-
alt_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
|
|
297
|
-
if alt_pip.exists():
|
|
298
|
-
venv_pip = alt_pip
|
|
299
|
-
if not venv_pip.exists():
|
|
300
|
-
# No venv, try system pip with --break-system-packages
|
|
301
|
-
try:
|
|
302
|
-
result = subprocess.run(
|
|
303
|
-
[sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
|
|
304
|
-
capture_output=True, text=True, timeout=120,
|
|
305
|
-
)
|
|
306
|
-
if result.returncode != 0:
|
|
307
|
-
return f"pip install failed: {result.stderr or result.stdout}"
|
|
308
|
-
except Exception as e:
|
|
309
|
-
return f"pip install error: {e}"
|
|
310
|
-
return None
|
|
311
|
-
try:
|
|
312
|
-
result = subprocess.run(
|
|
313
|
-
[str(venv_pip), "install", "--quiet", "-r", str(req_file)],
|
|
314
|
-
capture_output=True, text=True, timeout=120,
|
|
315
|
-
)
|
|
316
|
-
if result.returncode != 0:
|
|
317
|
-
return f"pip install failed: {result.stderr or result.stdout}"
|
|
318
|
-
except Exception as e:
|
|
319
|
-
return f"pip install error: {e}"
|
|
320
|
-
return None
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def _run_migrations() -> str | None:
|
|
324
|
-
"""Run init_db() to apply pending migrations. Returns error or None."""
|
|
325
|
-
# In packaged mode, db/ lives in NEXO_HOME; in dev mode, in SRC_DIR
|
|
326
|
-
cwd = str(NEXO_HOME) if _PACKAGED_INSTALL else str(SRC_DIR)
|
|
327
|
-
try:
|
|
328
|
-
result = subprocess.run(
|
|
329
|
-
[sys.executable, "-c", "import db; db.init_db()"],
|
|
330
|
-
cwd=cwd,
|
|
331
|
-
capture_output=True,
|
|
332
|
-
text=True,
|
|
333
|
-
timeout=30,
|
|
334
|
-
)
|
|
335
|
-
if result.returncode != 0:
|
|
336
|
-
return f"Migration failed: {result.stderr or result.stdout}"
|
|
337
|
-
except Exception as e:
|
|
338
|
-
return f"Migration error: {e}"
|
|
339
|
-
return None
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def _verify_import() -> str | None:
|
|
343
|
-
"""Verify server.py can be imported successfully."""
|
|
344
|
-
# In packaged mode, server.py lives in NEXO_HOME; in dev mode, in SRC_DIR
|
|
345
|
-
cwd = str(NEXO_HOME) if _PACKAGED_INSTALL else str(SRC_DIR)
|
|
346
|
-
try:
|
|
347
|
-
result = subprocess.run(
|
|
348
|
-
[sys.executable, "-c", "import server"],
|
|
349
|
-
cwd=cwd,
|
|
350
|
-
capture_output=True,
|
|
351
|
-
text=True,
|
|
352
|
-
timeout=15,
|
|
353
|
-
)
|
|
354
|
-
if result.returncode != 0:
|
|
355
|
-
return f"Import verification failed: {result.stderr or result.stdout}"
|
|
356
|
-
except Exception as e:
|
|
357
|
-
return f"Import verification error: {e}"
|
|
358
|
-
return None
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _sync_hooks_to_home():
|
|
362
|
-
"""Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after update."""
|
|
363
|
-
import shutil
|
|
364
|
-
hooks_src = SRC_DIR / "hooks"
|
|
365
|
-
hooks_dest = NEXO_HOME / "hooks"
|
|
366
|
-
if not hooks_src.is_dir():
|
|
367
|
-
return
|
|
368
|
-
hooks_dest.mkdir(parents=True, exist_ok=True)
|
|
369
|
-
synced = 0
|
|
370
|
-
for f in hooks_src.iterdir():
|
|
371
|
-
if f.is_file() and f.suffix == ".sh":
|
|
372
|
-
dest = hooks_dest / f.name
|
|
373
|
-
if not _paths_match(f, dest):
|
|
374
|
-
shutil.copy2(str(f), str(dest))
|
|
375
|
-
os.chmod(str(dest), 0o755)
|
|
376
|
-
synced += 1
|
|
377
|
-
if synced:
|
|
378
|
-
print(f"[NEXO update] Synced {synced} hook(s) to {hooks_dest}", file=sys.stderr)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def _backup_code_tree() -> tuple[str | None, str | None]:
|
|
382
|
-
"""Snapshot NEXO_HOME code dirs before npm update. Returns (backup_dir, error)."""
|
|
383
|
-
timestamp = time.strftime("%Y-%m-%d-%H%M%S")
|
|
384
|
-
backup_dir = BACKUP_BASE / f"code-tree-{timestamp}"
|
|
385
|
-
# Directories and flat files that postinstall copies into NEXO_HOME
|
|
386
|
-
code_dirs = [
|
|
387
|
-
"bin",
|
|
388
|
-
"hooks",
|
|
389
|
-
"plugins",
|
|
390
|
-
"db",
|
|
391
|
-
"cognitive",
|
|
392
|
-
"dashboard",
|
|
393
|
-
"rules",
|
|
394
|
-
"crons",
|
|
395
|
-
"scripts",
|
|
396
|
-
"doctor",
|
|
397
|
-
"skills",
|
|
398
|
-
"skills-core",
|
|
399
|
-
"skills-runtime",
|
|
400
|
-
"templates",
|
|
401
|
-
]
|
|
402
|
-
code_files_glob = ["*.py", "requirements.txt", "package.json"]
|
|
403
|
-
try:
|
|
404
|
-
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
405
|
-
# Backup directories
|
|
406
|
-
for d in code_dirs:
|
|
407
|
-
src = NEXO_HOME / d
|
|
408
|
-
if src.is_dir():
|
|
409
|
-
shutil.copytree(src, backup_dir / d, dirs_exist_ok=True)
|
|
410
|
-
# Backup flat code files in NEXO_HOME root
|
|
411
|
-
for pattern in code_files_glob:
|
|
412
|
-
for f in NEXO_HOME.glob(pattern):
|
|
413
|
-
if f.is_file():
|
|
414
|
-
shutil.copy2(f, backup_dir / f.name)
|
|
415
|
-
# Backup version.json
|
|
416
|
-
vf = NEXO_HOME / "version.json"
|
|
417
|
-
if vf.is_file():
|
|
418
|
-
shutil.copy2(vf, backup_dir / "version.json")
|
|
419
|
-
except Exception as e:
|
|
420
|
-
return None, f"Code tree backup failed: {e}"
|
|
421
|
-
return str(backup_dir), None
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def _restore_code_tree(backup_dir: str) -> str | None:
|
|
425
|
-
"""Restore NEXO_HOME code dirs from a backup snapshot. Returns error or None."""
|
|
426
|
-
bdir = Path(backup_dir)
|
|
427
|
-
if not bdir.is_dir():
|
|
428
|
-
return f"Code tree backup dir not found: {backup_dir}"
|
|
429
|
-
try:
|
|
430
|
-
for item in bdir.iterdir():
|
|
431
|
-
dest = NEXO_HOME / item.name
|
|
432
|
-
if item.is_dir():
|
|
433
|
-
if dest.is_dir():
|
|
434
|
-
shutil.rmtree(dest)
|
|
435
|
-
shutil.copytree(item, dest)
|
|
436
|
-
elif item.is_file():
|
|
437
|
-
shutil.copy2(item, dest)
|
|
438
|
-
except Exception as e:
|
|
439
|
-
return f"Code tree restore failed: {e}"
|
|
440
|
-
return None
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def _normalize_preferences_for_client_sync() -> dict:
|
|
444
|
-
from client_preferences import normalize_client_preferences
|
|
445
|
-
|
|
446
|
-
schedule_path = NEXO_HOME / "config" / "schedule.json"
|
|
447
|
-
schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
|
|
448
|
-
normalized_preferences = normalize_client_preferences(schedule_payload)
|
|
449
|
-
if normalized_preferences != {
|
|
450
|
-
key: schedule_payload.get(key)
|
|
451
|
-
for key in normalized_preferences
|
|
452
|
-
}:
|
|
453
|
-
merged_schedule = dict(schedule_payload)
|
|
454
|
-
merged_schedule.update(normalized_preferences)
|
|
455
|
-
schedule_path.parent.mkdir(parents=True, exist_ok=True)
|
|
456
|
-
schedule_path.write_text(json.dumps(merged_schedule, indent=2, ensure_ascii=False) + "\n")
|
|
457
|
-
return normalized_preferences
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
def _sync_packaged_clients() -> tuple[bool, str | None]:
|
|
461
|
-
try:
|
|
462
|
-
from client_sync import sync_all_clients
|
|
463
|
-
except Exception as e:
|
|
464
|
-
return False, f"client sync import failed: {e}"
|
|
465
|
-
|
|
466
|
-
try:
|
|
467
|
-
preferences = _normalize_preferences_for_client_sync()
|
|
468
|
-
result = sync_all_clients(
|
|
469
|
-
nexo_home=NEXO_HOME,
|
|
470
|
-
runtime_root=NEXO_HOME,
|
|
471
|
-
operator_name=os.environ.get("NEXO_NAME", ""),
|
|
472
|
-
preferences=preferences,
|
|
473
|
-
)
|
|
474
|
-
except Exception as e:
|
|
475
|
-
return False, f"client sync failed: {e}"
|
|
476
|
-
|
|
477
|
-
if result.get("ok"):
|
|
478
|
-
return True, None
|
|
479
|
-
|
|
480
|
-
clients = result.get("clients", {})
|
|
481
|
-
failures = []
|
|
482
|
-
for key, payload in clients.items():
|
|
483
|
-
if payload.get("ok") or payload.get("skipped"):
|
|
484
|
-
continue
|
|
485
|
-
failures.append(f"{key}: {payload.get('error', 'unknown error')}")
|
|
486
|
-
if not failures:
|
|
487
|
-
failures.append("unknown client sync failure")
|
|
488
|
-
return False, "; ".join(failures)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def _rollback_npm_package(target_version: str) -> str | None:
|
|
492
|
-
"""Rollback nexo-brain npm package to a specific version.
|
|
493
|
-
|
|
494
|
-
Uses NEXO_SKIP_POSTINSTALL because we restore the code tree
|
|
495
|
-
from our own pre-update backup — no need for postinstall migration.
|
|
496
|
-
"""
|
|
497
|
-
try:
|
|
498
|
-
result = subprocess.run(
|
|
499
|
-
["npm", "install", "-g", f"nexo-brain@{target_version}"],
|
|
500
|
-
capture_output=True, text=True, timeout=120,
|
|
501
|
-
env={**os.environ, "NEXO_SKIP_POSTINSTALL": "1", "NEXO_HOME": str(NEXO_HOME)},
|
|
502
|
-
)
|
|
503
|
-
if result.returncode != 0:
|
|
504
|
-
return f"npm rollback failed: {result.stderr or result.stdout}"
|
|
505
|
-
except Exception as e:
|
|
506
|
-
return f"npm rollback error: {e}"
|
|
507
|
-
return None
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def _emit_progress(progress_fn, message: str) -> None:
|
|
511
|
-
if callable(progress_fn):
|
|
512
|
-
try:
|
|
513
|
-
progress_fn(message)
|
|
514
|
-
except Exception:
|
|
515
|
-
pass
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def _paths_match(src: Path, dest: Path) -> bool:
|
|
519
|
-
try:
|
|
520
|
-
return src.exists() and dest.exists() and src.samefile(dest)
|
|
521
|
-
except Exception:
|
|
522
|
-
return False
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
def _sync_packaged_crons(progress_fn=None) -> tuple[bool, str | None]:
|
|
526
|
-
sync_path = NEXO_HOME / "crons" / "sync.py"
|
|
527
|
-
if not sync_path.is_file():
|
|
528
|
-
_refresh_installed_manifest()
|
|
529
|
-
return True, None
|
|
530
|
-
try:
|
|
531
|
-
_emit_progress(progress_fn, "Syncing core cron definitions...")
|
|
532
|
-
result = subprocess.run(
|
|
533
|
-
[sys.executable, str(sync_path)],
|
|
534
|
-
cwd=str(NEXO_HOME),
|
|
535
|
-
capture_output=True,
|
|
536
|
-
text=True,
|
|
537
|
-
timeout=30,
|
|
538
|
-
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(NEXO_HOME)},
|
|
539
|
-
)
|
|
540
|
-
if result.returncode != 0:
|
|
541
|
-
return False, result.stderr.strip() or result.stdout.strip() or "cron sync failed"
|
|
542
|
-
_refresh_installed_manifest()
|
|
543
|
-
return True, None
|
|
544
|
-
except Exception as e:
|
|
545
|
-
return False, f"cron sync error: {e}"
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
def _reload_launch_agents_after_bump() -> dict:
|
|
549
|
-
result: dict = {
|
|
550
|
-
"scanned": 0,
|
|
551
|
-
"reloaded": 0,
|
|
552
|
-
"skipped_missing": 0,
|
|
553
|
-
"errors": [],
|
|
554
|
-
"platform": sys.platform,
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if sys.platform != "darwin":
|
|
558
|
-
return result
|
|
559
|
-
|
|
560
|
-
launch_agents_dir = Path.home() / "Library" / "LaunchAgents"
|
|
561
|
-
if not launch_agents_dir.is_dir():
|
|
562
|
-
return result
|
|
563
|
-
|
|
564
|
-
try:
|
|
565
|
-
plists = sorted(launch_agents_dir.glob("com.nexo.*.plist"))
|
|
566
|
-
except Exception as e:
|
|
567
|
-
result["errors"].append({"plist": "*", "stderr": f"glob failed: {e}"})
|
|
568
|
-
return result
|
|
569
|
-
|
|
570
|
-
result["scanned"] = len(plists)
|
|
571
|
-
for plist in plists:
|
|
572
|
-
try:
|
|
573
|
-
if not plist.is_file():
|
|
574
|
-
result["skipped_missing"] += 1
|
|
575
|
-
continue
|
|
576
|
-
subprocess.run(
|
|
577
|
-
["launchctl", "unload", str(plist)],
|
|
578
|
-
capture_output=True,
|
|
579
|
-
text=True,
|
|
580
|
-
timeout=10,
|
|
581
|
-
)
|
|
582
|
-
load_proc = subprocess.run(
|
|
583
|
-
["launchctl", "load", "-w", str(plist)],
|
|
584
|
-
capture_output=True,
|
|
585
|
-
text=True,
|
|
586
|
-
timeout=10,
|
|
587
|
-
)
|
|
588
|
-
if load_proc.returncode == 0:
|
|
589
|
-
result["reloaded"] += 1
|
|
590
|
-
else:
|
|
591
|
-
result["errors"].append(
|
|
592
|
-
{
|
|
593
|
-
"plist": plist.name,
|
|
594
|
-
"stderr": (load_proc.stderr or load_proc.stdout or "load failed")[:300],
|
|
595
|
-
}
|
|
596
|
-
)
|
|
597
|
-
except subprocess.TimeoutExpired:
|
|
598
|
-
result["errors"].append({"plist": plist.name, "stderr": "launchctl timeout"})
|
|
599
|
-
except Exception as e:
|
|
600
|
-
result["errors"].append({"plist": plist.name, "stderr": str(e)[:300]})
|
|
601
|
-
|
|
602
|
-
return result
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
def _handle_packaged_update(progress_fn=None) -> str:
|
|
606
|
-
"""Update a packaged (npm) install — no git repo available."""
|
|
607
|
-
old_version = _read_version()
|
|
608
|
-
|
|
609
|
-
# 1. Backup databases BEFORE any changes
|
|
610
|
-
_emit_progress(progress_fn, "Backing up runtime databases...")
|
|
611
|
-
backup_dir, backup_err = _backup_databases()
|
|
612
|
-
if backup_err:
|
|
613
|
-
return f"ABORTED at backup: {backup_err}"
|
|
614
|
-
|
|
615
|
-
# 2. Backup NEXO_HOME code tree BEFORE npm update
|
|
616
|
-
# postinstall copies hooks/core/plugins/scripts into NEXO_HOME,
|
|
617
|
-
# so we need a full snapshot to restore on failure.
|
|
618
|
-
_emit_progress(progress_fn, "Backing up runtime files...")
|
|
619
|
-
code_backup_dir, code_err = _backup_code_tree()
|
|
620
|
-
if code_err:
|
|
621
|
-
return f"ABORTED at code tree backup: {code_err}"
|
|
622
|
-
|
|
623
|
-
# 3. Run npm update (postinstall.js will migrate NEXO_HOME in-place)
|
|
624
|
-
try:
|
|
625
|
-
_emit_progress(progress_fn, "Downloading and applying the latest npm package...")
|
|
626
|
-
result = subprocess.run(
|
|
627
|
-
["npm", "update", "-g", "nexo-brain"],
|
|
628
|
-
capture_output=True, text=True, timeout=120,
|
|
629
|
-
env={**os.environ, "NEXO_HOME": str(NEXO_HOME)},
|
|
630
|
-
)
|
|
631
|
-
if result.returncode != 0:
|
|
632
|
-
# npm failed (including postinstall failures) — full rollback
|
|
633
|
-
if backup_dir:
|
|
634
|
-
_restore_databases(backup_dir)
|
|
635
|
-
if code_backup_dir:
|
|
636
|
-
_restore_code_tree(code_backup_dir)
|
|
637
|
-
# Reinstall pip deps from restored old requirements.txt
|
|
638
|
-
_reinstall_pip_deps()
|
|
639
|
-
rollback_err = _rollback_npm_package(old_version)
|
|
640
|
-
msg = f"ABORTED: npm update failed: {result.stderr or result.stdout}"
|
|
641
|
-
if rollback_err:
|
|
642
|
-
msg += f"\n WARNING: npm rollback also failed: {rollback_err}"
|
|
643
|
-
msg += f"\n Manual rollback: npm install -g nexo-brain@{old_version}"
|
|
644
|
-
return msg
|
|
645
|
-
except FileNotFoundError:
|
|
646
|
-
return "ABORTED: npm not found. Install Node.js to update packaged installs."
|
|
647
|
-
except Exception as e:
|
|
648
|
-
if backup_dir:
|
|
649
|
-
_restore_databases(backup_dir)
|
|
650
|
-
if code_backup_dir:
|
|
651
|
-
_restore_code_tree(code_backup_dir)
|
|
652
|
-
# Reinstall pip deps from restored old requirements.txt
|
|
653
|
-
_reinstall_pip_deps()
|
|
654
|
-
rollback_err = _rollback_npm_package(old_version)
|
|
655
|
-
msg = f"ABORTED: npm update error: {e}"
|
|
656
|
-
if rollback_err:
|
|
657
|
-
msg += f"\n WARNING: npm rollback also failed: {rollback_err}"
|
|
658
|
-
msg += f"\n Manual rollback: npm install -g nexo-brain@{old_version}"
|
|
659
|
-
return msg
|
|
660
|
-
|
|
661
|
-
new_version = _read_version()
|
|
662
|
-
if old_version == new_version:
|
|
663
|
-
return f"Already up to date (v{old_version}). No changes."
|
|
664
|
-
|
|
665
|
-
# 4. Post-npm verification steps
|
|
666
|
-
errors = []
|
|
667
|
-
|
|
668
|
-
# Reinstall pip deps for new version
|
|
669
|
-
_emit_progress(progress_fn, "Reconciling Python dependencies...")
|
|
670
|
-
pip_err = _reinstall_pip_deps()
|
|
671
|
-
if pip_err:
|
|
672
|
-
errors.append(f"pip deps: {pip_err}")
|
|
673
|
-
|
|
674
|
-
# Run migrations
|
|
675
|
-
_emit_progress(progress_fn, "Running runtime migrations...")
|
|
676
|
-
mig_err = _run_migrations()
|
|
677
|
-
if mig_err:
|
|
678
|
-
errors.append(f"migrations: {mig_err}")
|
|
679
|
-
|
|
680
|
-
# Verify server can still import
|
|
681
|
-
_emit_progress(progress_fn, "Verifying runtime import health...")
|
|
682
|
-
verify_err = _verify_import()
|
|
683
|
-
if verify_err:
|
|
684
|
-
errors.append(f"verification: {verify_err}")
|
|
685
|
-
|
|
686
|
-
hook_sync_warning = None
|
|
687
|
-
cron_sync_warning = None
|
|
688
|
-
retired_runtime_files: list[str] = []
|
|
689
|
-
launchagent_reload_warning = None
|
|
690
|
-
launchagent_reload_summary = None
|
|
691
|
-
cron_sync_ok, cron_sync_error = _sync_packaged_crons(progress_fn=progress_fn)
|
|
692
|
-
if not cron_sync_ok:
|
|
693
|
-
errors.append(f"cron sync: {cron_sync_error}")
|
|
694
|
-
cron_sync_warning = cron_sync_error
|
|
695
|
-
try:
|
|
696
|
-
_emit_progress(progress_fn, "Refreshing installed hooks and manifests...")
|
|
697
|
-
_sync_hooks_to_home()
|
|
698
|
-
retired_runtime_files = _cleanup_retired_runtime_files()
|
|
699
|
-
except Exception as e:
|
|
700
|
-
hook_sync_warning = f"{e}"
|
|
701
|
-
|
|
702
|
-
client_sync_warning = None
|
|
703
|
-
_emit_progress(progress_fn, "Refreshing shared client configs...")
|
|
704
|
-
clients_ok, client_sync_error = _sync_packaged_clients()
|
|
705
|
-
if not clients_ok:
|
|
706
|
-
client_sync_warning = client_sync_error or "unknown client sync error"
|
|
707
|
-
|
|
708
|
-
if old_version != new_version:
|
|
709
|
-
_emit_progress(progress_fn, "Reloading LaunchAgents after version bump...")
|
|
710
|
-
try:
|
|
711
|
-
launchagent_reload_summary = _reload_launch_agents_after_bump()
|
|
712
|
-
if launchagent_reload_summary.get("errors"):
|
|
713
|
-
launchagent_reload_warning = (
|
|
714
|
-
f"reloaded {launchagent_reload_summary['reloaded']}/"
|
|
715
|
-
f"{launchagent_reload_summary['scanned']} with "
|
|
716
|
-
f"{len(launchagent_reload_summary['errors'])} error(s)"
|
|
717
|
-
)
|
|
718
|
-
except Exception as e:
|
|
719
|
-
launchagent_reload_warning = f"launchagent reload error: {e}"
|
|
720
|
-
|
|
721
|
-
if errors:
|
|
722
|
-
# 5. Full rollback: restore code tree + DBs + pip deps + rollback npm package
|
|
723
|
-
if code_backup_dir:
|
|
724
|
-
tree_err = _restore_code_tree(code_backup_dir)
|
|
725
|
-
else:
|
|
726
|
-
tree_err = "no code tree backup available"
|
|
727
|
-
if backup_dir:
|
|
728
|
-
_restore_databases(backup_dir)
|
|
729
|
-
# Reinstall pip deps from the restored (old) requirements.txt
|
|
730
|
-
# so the venv matches the rolled-back code tree
|
|
731
|
-
pip_rollback_err = _reinstall_pip_deps() if not tree_err else None
|
|
732
|
-
rollback_err = _rollback_npm_package(old_version)
|
|
733
|
-
lines = [f"UPDATE FAILED (packaged install, v{old_version} -> v{new_version})"]
|
|
734
|
-
for err in errors:
|
|
735
|
-
lines.append(f" ERROR: {err}")
|
|
736
|
-
lines.append(f" Databases restored from: {backup_dir}")
|
|
737
|
-
if tree_err:
|
|
738
|
-
lines.append(f" WARNING: code tree restore failed: {tree_err}")
|
|
739
|
-
else:
|
|
740
|
-
lines.append(f" Code tree restored from: {code_backup_dir}")
|
|
741
|
-
if pip_rollback_err:
|
|
742
|
-
lines.append(f" WARNING: pip deps rollback failed: {pip_rollback_err}")
|
|
743
|
-
elif not tree_err:
|
|
744
|
-
lines.append(" Python deps: reinstalled from old requirements.txt")
|
|
745
|
-
if rollback_err:
|
|
746
|
-
lines.append(f" WARNING: npm rollback failed: {rollback_err}")
|
|
747
|
-
lines.append(f" Manual rollback: npm install -g nexo-brain@{old_version}")
|
|
748
|
-
else:
|
|
749
|
-
lines.append(f" npm package rolled back to v{old_version}")
|
|
750
|
-
lines.append("")
|
|
751
|
-
lines.append("Fix the errors above, then run nexo_update again.")
|
|
752
|
-
return "\n".join(lines)
|
|
753
|
-
|
|
754
|
-
lines = ["UPDATE SUCCESSFUL (packaged install)"]
|
|
755
|
-
lines.append(f" Version: {old_version} -> {new_version}")
|
|
756
|
-
lines.append(f" Backup: {backup_dir}")
|
|
757
|
-
if not cron_sync_warning:
|
|
758
|
-
lines.append(" Crons: synced with manifest")
|
|
759
|
-
else:
|
|
760
|
-
lines.append(f" WARNING: cron sync: {cron_sync_warning}")
|
|
761
|
-
if not hook_sync_warning:
|
|
762
|
-
lines.append(" Hooks: synced to NEXO_HOME")
|
|
763
|
-
else:
|
|
764
|
-
lines.append(f" WARNING: hook sync: {hook_sync_warning}")
|
|
765
|
-
if retired_runtime_files:
|
|
766
|
-
lines.append(f" Cleanup: removed {len(retired_runtime_files)} retired runtime file(s)")
|
|
767
|
-
if not client_sync_warning:
|
|
768
|
-
lines.append(" Clients: configured client targets synced")
|
|
769
|
-
else:
|
|
770
|
-
lines.append(f" WARNING: client sync: {client_sync_warning}")
|
|
771
|
-
if launchagent_reload_summary and launchagent_reload_summary.get("scanned"):
|
|
772
|
-
if not launchagent_reload_warning:
|
|
773
|
-
lines.append(
|
|
774
|
-
" LaunchAgents: reloaded "
|
|
775
|
-
f"{launchagent_reload_summary['reloaded']}/"
|
|
776
|
-
f"{launchagent_reload_summary['scanned']}"
|
|
777
|
-
)
|
|
778
|
-
else:
|
|
779
|
-
lines.append(f" WARNING: launchagent reload: {launchagent_reload_warning}")
|
|
780
|
-
lines.append("")
|
|
781
|
-
lines.append("MCP server restart needed to load new code.")
|
|
782
|
-
return "\n".join(lines)
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
def handle_update(remote: str = "origin", branch: str = "main", progress_fn=None) -> str:
|
|
786
|
-
"""Pull latest NEXO code, backup databases, run migrations, and verify.
|
|
787
|
-
|
|
788
|
-
Supports both git checkouts and packaged (npm) installs.
|
|
789
|
-
|
|
790
|
-
Full update flow (git):
|
|
791
|
-
1. Check for uncommitted changes in entire worktree
|
|
792
|
-
2. Backup all .db files
|
|
793
|
-
3. git pull
|
|
794
|
-
4. Reinstall Python dependencies if version changed
|
|
795
|
-
5. Run migrations if version changed
|
|
796
|
-
6. Verify server.py imports
|
|
797
|
-
7. Rollback on failure (git reset --hard to saved commit)
|
|
798
|
-
|
|
799
|
-
Args:
|
|
800
|
-
remote: Git remote name (default: origin)
|
|
801
|
-
branch: Git branch to pull (default: main)
|
|
802
|
-
"""
|
|
803
|
-
# Packaged install — no git repo
|
|
804
|
-
if not _is_git_repo():
|
|
805
|
-
return _handle_packaged_update(progress_fn=progress_fn)
|
|
806
|
-
|
|
807
|
-
steps_done = []
|
|
808
|
-
old_commit = None
|
|
809
|
-
backup_dir = None
|
|
810
|
-
|
|
811
|
-
try:
|
|
812
|
-
# Step 1: Check dirty (full worktree)
|
|
813
|
-
_emit_progress(progress_fn, "Checking repository state...")
|
|
814
|
-
dirty_err = _check_dirty()
|
|
815
|
-
if dirty_err:
|
|
816
|
-
return f"ABORTED: {dirty_err}"
|
|
817
|
-
steps_done.append("clean-check")
|
|
818
|
-
|
|
819
|
-
# Record current state
|
|
820
|
-
old_version = _read_version()
|
|
821
|
-
old_req_hash = _requirements_hash()
|
|
822
|
-
rc, old_commit, _ = _git("rev-parse", "HEAD")
|
|
823
|
-
if rc != 0:
|
|
824
|
-
return "ABORTED: Not a git repository or git not available."
|
|
825
|
-
|
|
826
|
-
# Step 2: Backup databases
|
|
827
|
-
_emit_progress(progress_fn, "Backing up runtime databases...")
|
|
828
|
-
backup_dir, backup_err = _backup_databases()
|
|
829
|
-
if backup_err:
|
|
830
|
-
return f"ABORTED at backup: {backup_err}"
|
|
831
|
-
steps_done.append("backup")
|
|
832
|
-
|
|
833
|
-
# Step 3: git pull
|
|
834
|
-
_emit_progress(progress_fn, "Pulling latest source changes...")
|
|
835
|
-
rc, pull_out, pull_err = _git("pull", remote, branch)
|
|
836
|
-
if rc != 0:
|
|
837
|
-
return f"ABORTED at git pull: {pull_err or pull_out}"
|
|
838
|
-
steps_done.append("git-pull")
|
|
839
|
-
|
|
840
|
-
# Step 4: Check version and dependency changes
|
|
841
|
-
new_version = _read_version()
|
|
842
|
-
version_changed = old_version != new_version
|
|
843
|
-
new_req_hash = _requirements_hash()
|
|
844
|
-
deps_changed = old_req_hash != new_req_hash
|
|
845
|
-
|
|
846
|
-
# Step 5: Reinstall pip dependencies if requirements.txt changed
|
|
847
|
-
if deps_changed or version_changed:
|
|
848
|
-
_emit_progress(progress_fn, "Reconciling Python dependencies...")
|
|
849
|
-
pip_err = _reinstall_pip_deps()
|
|
850
|
-
if pip_err:
|
|
851
|
-
raise RuntimeError(f"Pip install failed: {pip_err}")
|
|
852
|
-
steps_done.append("pip-deps")
|
|
853
|
-
|
|
854
|
-
# Step 6: Run migrations if version changed
|
|
855
|
-
if version_changed:
|
|
856
|
-
_emit_progress(progress_fn, "Running runtime migrations...")
|
|
857
|
-
mig_err = _run_migrations()
|
|
858
|
-
if mig_err:
|
|
859
|
-
raise RuntimeError(f"Migration failed: {mig_err}")
|
|
860
|
-
steps_done.append("migrations")
|
|
861
|
-
|
|
862
|
-
# Step 7: Verify import
|
|
863
|
-
_emit_progress(progress_fn, "Verifying runtime import health...")
|
|
864
|
-
verify_err = _verify_import()
|
|
865
|
-
if verify_err:
|
|
866
|
-
raise RuntimeError(f"Verification failed: {verify_err}")
|
|
867
|
-
steps_done.append("verify")
|
|
868
|
-
|
|
869
|
-
# Step 8: Sync crons with manifest
|
|
870
|
-
cron_sync_result = ""
|
|
871
|
-
try:
|
|
872
|
-
cron_sync_path = SRC_DIR / "crons" / "sync.py"
|
|
873
|
-
if cron_sync_path.exists():
|
|
874
|
-
_emit_progress(progress_fn, "Syncing core cron definitions...")
|
|
875
|
-
r = subprocess.run(
|
|
876
|
-
[sys.executable, str(cron_sync_path)],
|
|
877
|
-
capture_output=True, text=True, timeout=30,
|
|
878
|
-
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(SRC_DIR)},
|
|
879
|
-
)
|
|
880
|
-
cron_sync_result = r.stdout.strip()
|
|
881
|
-
if r.returncode == 0:
|
|
882
|
-
steps_done.append("cron-sync")
|
|
883
|
-
# Refresh installed manifest only after successful sync
|
|
884
|
-
_refresh_installed_manifest()
|
|
885
|
-
else:
|
|
886
|
-
cron_sync_result = f"Cron sync failed (exit {r.returncode}): {r.stderr or r.stdout}"
|
|
887
|
-
except Exception as e:
|
|
888
|
-
cron_sync_result = f"Cron sync warning: {e}"
|
|
889
|
-
|
|
890
|
-
# Step 9: Sync hooks to NEXO_HOME
|
|
891
|
-
retired_runtime_files: list[str] = []
|
|
892
|
-
try:
|
|
893
|
-
_emit_progress(progress_fn, "Syncing core Claude hooks...")
|
|
894
|
-
_sync_hooks_to_home()
|
|
895
|
-
retired_runtime_files = _cleanup_retired_runtime_files()
|
|
896
|
-
steps_done.append("hook-sync")
|
|
897
|
-
except Exception as e:
|
|
898
|
-
pass # Non-critical, log in function
|
|
899
|
-
|
|
900
|
-
# Step 10: Sync shared client configs
|
|
901
|
-
try:
|
|
902
|
-
_emit_progress(progress_fn, "Refreshing shared client configs...")
|
|
903
|
-
from client_sync import sync_all_clients
|
|
904
|
-
from client_preferences import normalize_client_preferences
|
|
905
|
-
|
|
906
|
-
schedule_path = NEXO_HOME / "config" / "schedule.json"
|
|
907
|
-
schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
|
|
908
|
-
normalized_preferences = normalize_client_preferences(schedule_payload)
|
|
909
|
-
if normalized_preferences != {
|
|
910
|
-
key: schedule_payload.get(key)
|
|
911
|
-
for key in normalized_preferences
|
|
912
|
-
}:
|
|
913
|
-
merged_schedule = dict(schedule_payload)
|
|
914
|
-
merged_schedule.update(normalized_preferences)
|
|
915
|
-
schedule_path.parent.mkdir(parents=True, exist_ok=True)
|
|
916
|
-
schedule_path.write_text(json.dumps(merged_schedule, indent=2, ensure_ascii=False) + "\n")
|
|
917
|
-
client_sync_result = sync_all_clients(
|
|
918
|
-
nexo_home=NEXO_HOME,
|
|
919
|
-
runtime_root=SRC_DIR,
|
|
920
|
-
operator_name=os.environ.get("NEXO_NAME", ""),
|
|
921
|
-
preferences=normalized_preferences,
|
|
922
|
-
)
|
|
923
|
-
if client_sync_result.get("ok"):
|
|
924
|
-
steps_done.append("client-sync")
|
|
925
|
-
except Exception:
|
|
926
|
-
pass # Non-critical, configs can be re-synced later
|
|
927
|
-
|
|
928
|
-
# Build result
|
|
929
|
-
if pull_out == "Already up to date.":
|
|
930
|
-
return f"Already up to date (v{old_version}). No changes pulled."
|
|
931
|
-
|
|
932
|
-
lines = ["UPDATE SUCCESSFUL"]
|
|
933
|
-
if version_changed:
|
|
934
|
-
lines.append(f" Version: {old_version} -> {new_version}")
|
|
935
|
-
else:
|
|
936
|
-
lines.append(f" Version: {old_version} (unchanged)")
|
|
937
|
-
lines.append(f" Branch: {remote}/{branch}")
|
|
938
|
-
lines.append(f" Backup: {backup_dir}")
|
|
939
|
-
if "pip-deps" in steps_done:
|
|
940
|
-
lines.append(" Python deps: reinstalled")
|
|
941
|
-
if version_changed:
|
|
942
|
-
lines.append(" Migrations: applied")
|
|
943
|
-
if "cron-sync" in steps_done:
|
|
944
|
-
lines.append(" Crons: synced with manifest")
|
|
945
|
-
if "hook-sync" in steps_done:
|
|
946
|
-
lines.append(" Hooks: synced to NEXO_HOME")
|
|
947
|
-
if retired_runtime_files:
|
|
948
|
-
lines.append(f" Cleanup: removed {len(retired_runtime_files)} retired runtime file(s)")
|
|
949
|
-
if "client-sync" in steps_done:
|
|
950
|
-
lines.append(" Clients: configured client targets synced")
|
|
951
|
-
lines.append("")
|
|
952
|
-
lines.append("MCP server restart needed to load new code.")
|
|
953
|
-
return "\n".join(lines)
|
|
954
|
-
|
|
955
|
-
except Exception as e:
|
|
956
|
-
# Rollback — use git checkout to saved commit (safer than reset --hard)
|
|
957
|
-
rollback_lines = [f"UPDATE FAILED: {e}", "", "Rolling back..."]
|
|
958
|
-
|
|
959
|
-
if old_commit and "git-pull" in steps_done:
|
|
960
|
-
# Full rollback: reset HEAD + index + worktree to old commit
|
|
961
|
-
rc, _, err = _git("reset", "--hard", old_commit)
|
|
962
|
-
if rc == 0:
|
|
963
|
-
rollback_lines.append(f" Git: restored files to {old_commit[:8]}")
|
|
964
|
-
# Reinstall pip deps from the restored old requirements.txt
|
|
965
|
-
# so the venv matches the rolled-back code
|
|
966
|
-
if "pip-deps" in steps_done:
|
|
967
|
-
pip_rb_err = _reinstall_pip_deps()
|
|
968
|
-
if pip_rb_err:
|
|
969
|
-
rollback_lines.append(f" WARNING: pip deps rollback failed: {pip_rb_err}")
|
|
970
|
-
else:
|
|
971
|
-
rollback_lines.append(" Python deps: reinstalled from old requirements.txt")
|
|
972
|
-
else:
|
|
973
|
-
rollback_lines.append(f" Git rollback FAILED: {err}")
|
|
974
|
-
|
|
975
|
-
if backup_dir and "backup" in steps_done:
|
|
976
|
-
_restore_databases(backup_dir)
|
|
977
|
-
rollback_lines.append(f" DBs: restored from {backup_dir}")
|
|
978
|
-
|
|
979
|
-
rollback_lines.append("")
|
|
980
|
-
rollback_lines.append("System restored to previous state.")
|
|
981
|
-
return "\n".join(rollback_lines)
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
TOOLS = [
|
|
985
|
-
(handle_update, "nexo_update", "Pull latest NEXO code, backup DBs, run migrations, verify. Rolls back on failure."),
|
|
986
|
-
]
|