nexo-brain 2.6.11 → 2.6.13
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 +22 -12
- package/bin/nexo-brain.js +483 -56
- package/package.json +4 -1
- package/src/agent_runner.py +322 -0
- package/src/auto_update.py +12 -3
- package/src/cli.py +22 -10
- package/src/client_preferences.py +394 -0
- package/src/client_sync.py +78 -0
- package/src/cron_recovery.py +8 -1
- package/src/crons/manifest.json +6 -0
- package/src/crons/sync.py +14 -1
- package/src/doctor/providers/runtime.py +109 -1
- package/src/plugins/schedule.py +69 -12
- package/src/plugins/update.py +5 -1
- package/src/runtime_power.py +23 -0
- package/src/script_registry.py +62 -1
- package/src/scripts/check-context.py +102 -100
- package/src/scripts/deep-sleep/extract.py +29 -54
- package/src/scripts/deep-sleep/synthesize.py +14 -38
- package/src/scripts/nexo-agent-run.py +73 -0
- package/src/scripts/nexo-catchup.py +15 -19
- package/src/scripts/nexo-daily-self-audit.py +17 -14
- package/src/scripts/nexo-evolution-run.py +25 -55
- package/src/scripts/nexo-immune.py +17 -15
- package/src/scripts/nexo-learning-validator.py +90 -58
- package/src/scripts/nexo-postmortem-consolidator.py +15 -14
- package/src/scripts/nexo-sleep.py +20 -14
- package/src/scripts/nexo-synthesis.py +19 -12
- package/src/scripts/nexo-update.sh +28 -2
- package/src/scripts/nexo-watchdog.sh +34 -10
- package/templates/nexo_helper.py +45 -0
- package/templates/plugin-template.py +4 -0
- package/templates/script-template.py +13 -2
- package/templates/skill-script-template.py +8 -0
|
@@ -4,7 +4,7 @@ NEXO Sleep System v2 — The brain dreams.
|
|
|
4
4
|
|
|
5
5
|
Before: 834 lines with word-overlap "intelligence" for learning consolidation.
|
|
6
6
|
Now: Stage A (mechanical cleanup) stays pure Python. Stage B (dreaming) uses
|
|
7
|
-
|
|
7
|
+
the configured automation backend to understand, deduplicate, and prune with real intelligence.
|
|
8
8
|
|
|
9
9
|
Triggered hourly via LaunchAgent. Runs ONCE per day, first time Mac is awake.
|
|
10
10
|
If interrupted (power loss, crash), resumes on next trigger.
|
|
@@ -12,7 +12,7 @@ If interrupted (power loss, crash), resumes on next trigger.
|
|
|
12
12
|
Stage A — Housekeeping (Python pure):
|
|
13
13
|
Delete old logs, rotate files, trim JSON. No intelligence needed.
|
|
14
14
|
|
|
15
|
-
Stage B — Dreaming (
|
|
15
|
+
Stage B — Dreaming (automation backend):
|
|
16
16
|
Review learnings for duplicates and contradictions with UNDERSTANDING.
|
|
17
17
|
Prune MEMORY.md if over limit. Clean preferences. Compress old observations.
|
|
18
18
|
One CLI call that does what 500 lines of word-overlap couldn't.
|
|
@@ -30,6 +30,13 @@ from datetime import datetime, date, timedelta
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
32
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
33
|
+
_script_dir = Path(__file__).resolve().parent
|
|
34
|
+
_repo_src = _script_dir.parent
|
|
35
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
36
|
+
if str(NEXO_CODE) not in sys.path:
|
|
37
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
38
|
+
|
|
39
|
+
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
33
40
|
|
|
34
41
|
# ─── Paths ────────────────────────────────────────────────────────────────────
|
|
35
42
|
CLAUDE_DIR = NEXO_HOME
|
|
@@ -293,7 +300,7 @@ def stage_a_cleanup() -> dict:
|
|
|
293
300
|
return stats
|
|
294
301
|
|
|
295
302
|
|
|
296
|
-
# ─── Stage B: Dreaming (
|
|
303
|
+
# ─── Stage B: Dreaming (automation backend) ─────────────────────────────────
|
|
297
304
|
|
|
298
305
|
def collect_brain_state() -> dict:
|
|
299
306
|
"""Collect all data the CLI needs to dream."""
|
|
@@ -428,18 +435,14 @@ ABSOLUTE RULES:
|
|
|
428
435
|
Write a summary to {COORD_DIR}/sleep-report.md when done.
|
|
429
436
|
Execute without asking."""
|
|
430
437
|
|
|
431
|
-
log("Stage B: Invoking
|
|
432
|
-
env = os.environ.copy()
|
|
433
|
-
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
434
|
-
env.pop("CLAUDECODE", None)
|
|
435
|
-
env.pop("CLAUDE_CODE", None)
|
|
436
|
-
|
|
438
|
+
log("Stage B: Invoking automation backend — dreaming...")
|
|
437
439
|
try:
|
|
438
|
-
result =
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
result = run_automation_prompt(
|
|
441
|
+
prompt,
|
|
442
|
+
model="opus",
|
|
443
|
+
timeout=21600,
|
|
444
|
+
output_format="text",
|
|
445
|
+
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
443
446
|
)
|
|
444
447
|
|
|
445
448
|
if result.returncode != 0:
|
|
@@ -449,6 +452,9 @@ Execute without asking."""
|
|
|
449
452
|
log(f"Stage B: Dreaming complete. Output: {len(result.stdout or '')} chars")
|
|
450
453
|
return {"ok": True, "output_len": len(result.stdout or "")}
|
|
451
454
|
|
|
455
|
+
except AutomationBackendUnavailableError as e:
|
|
456
|
+
log(f"Stage B: automation backend unavailable: {e}")
|
|
457
|
+
return {"error": "backend-unavailable"}
|
|
452
458
|
except subprocess.TimeoutExpired:
|
|
453
459
|
log("Stage B: CLI timed out (600s)")
|
|
454
460
|
return {"error": "timeout"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
NEXO Synthesis Engine v2 — Daily intelligence brief.
|
|
4
4
|
|
|
5
5
|
Before: ~400 lines of Python concatenating SQL results into markdown sections.
|
|
6
|
-
Now: Collects raw data, passes to
|
|
6
|
+
Now: Collects raw data, passes to the configured automation backend which synthesizes
|
|
7
7
|
with real understanding of what matters for tomorrow.
|
|
8
8
|
|
|
9
9
|
Runs daily at 06:00 via LaunchAgent.
|
|
@@ -20,6 +20,14 @@ from pathlib import Path
|
|
|
20
20
|
|
|
21
21
|
HOME = Path.home()
|
|
22
22
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
23
|
+
_script_dir = Path(__file__).resolve().parent
|
|
24
|
+
_repo_src = _script_dir.parent
|
|
25
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
26
|
+
if str(NEXO_CODE) not in sys.path:
|
|
27
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
28
|
+
|
|
29
|
+
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
30
|
+
|
|
23
31
|
CLAUDE_DIR = NEXO_HOME
|
|
24
32
|
COORD_DIR = CLAUDE_DIR / "coordination"
|
|
25
33
|
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
@@ -204,18 +212,14 @@ not what merely happened. If a section has nothing, write "Nothing notable."
|
|
|
204
212
|
|
|
205
213
|
Execute without asking."""
|
|
206
214
|
|
|
207
|
-
log("Invoking
|
|
208
|
-
env = os.environ.copy()
|
|
209
|
-
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
210
|
-
env.pop("CLAUDECODE", None)
|
|
211
|
-
env.pop("CLAUDE_CODE", None)
|
|
212
|
-
|
|
215
|
+
log("Invoking automation backend for synthesis...")
|
|
213
216
|
try:
|
|
214
|
-
result =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
result = run_automation_prompt(
|
|
218
|
+
prompt,
|
|
219
|
+
model="opus",
|
|
220
|
+
timeout=21600,
|
|
221
|
+
output_format="text",
|
|
222
|
+
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
219
223
|
)
|
|
220
224
|
|
|
221
225
|
if result.returncode != 0:
|
|
@@ -225,6 +229,9 @@ Execute without asking."""
|
|
|
225
229
|
log(f"Synthesis complete. Output: {len(result.stdout or '')} chars")
|
|
226
230
|
return True
|
|
227
231
|
|
|
232
|
+
except AutomationBackendUnavailableError as e:
|
|
233
|
+
log(f"Automation backend unavailable: {e}")
|
|
234
|
+
return False
|
|
228
235
|
except subprocess.TimeoutExpired:
|
|
229
236
|
log("CLI timed out (180s)")
|
|
230
237
|
return False
|
|
@@ -250,8 +250,34 @@ fi
|
|
|
250
250
|
# --- Step 9: Sync shared client configs ---
|
|
251
251
|
CLIENT_SYNC="$SRC_DIR/scripts/nexo-sync-clients.py"
|
|
252
252
|
if [ -f "$CLIENT_SYNC" ]; then
|
|
253
|
-
log "Syncing
|
|
254
|
-
|
|
253
|
+
log "Syncing shared client configs..."
|
|
254
|
+
CLIENT_SYNC_ARGS=()
|
|
255
|
+
if [ -f "$NEXO_HOME/config/schedule.json" ]; then
|
|
256
|
+
while IFS= read -r line; do
|
|
257
|
+
[ -n "$line" ] && CLIENT_SYNC_ARGS+=("--enabled-client" "$line")
|
|
258
|
+
done < <(
|
|
259
|
+
NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 - <<'PY'
|
|
260
|
+
import json
|
|
261
|
+
import os
|
|
262
|
+
import sys
|
|
263
|
+
from pathlib import Path
|
|
264
|
+
|
|
265
|
+
sys.path.insert(0, os.environ["NEXO_CODE"])
|
|
266
|
+
from client_preferences import normalize_client_preferences
|
|
267
|
+
|
|
268
|
+
schedule_file = Path(os.environ["NEXO_HOME"]) / "config" / "schedule.json"
|
|
269
|
+
prefs = normalize_client_preferences(json.loads(schedule_file.read_text())) if schedule_file.exists() else normalize_client_preferences({})
|
|
270
|
+
enabled = [key for key, value in prefs.get("interactive_clients", {}).items() if value]
|
|
271
|
+
if prefs.get("automation_enabled", True):
|
|
272
|
+
backend = prefs.get("automation_backend")
|
|
273
|
+
if backend and backend != "none" and backend not in enabled:
|
|
274
|
+
enabled.append(backend)
|
|
275
|
+
for item in enabled:
|
|
276
|
+
print(item)
|
|
277
|
+
PY
|
|
278
|
+
)
|
|
279
|
+
fi
|
|
280
|
+
if NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 "$CLIENT_SYNC" --nexo-home "$NEXO_HOME" --runtime-root "$SRC_DIR" "${CLIENT_SYNC_ARGS[@]}" --json >/dev/null 2>&1; then
|
|
255
281
|
log "Shared client configs synced."
|
|
256
282
|
else
|
|
257
283
|
warn "Client config sync failed (non-fatal). Run 'nexo clients sync' later."
|
|
@@ -64,7 +64,9 @@ import json, sys, platform
|
|
|
64
64
|
nexo_home = '$NEXO_HOME'
|
|
65
65
|
is_mac = platform.system() == 'Darwin'
|
|
66
66
|
optionals_file = '$NEXO_HOME/config/optionals.json'
|
|
67
|
+
schedule_file = '$NEXO_HOME/config/schedule.json'
|
|
67
68
|
optionals = {}
|
|
69
|
+
automation_default = True
|
|
68
70
|
|
|
69
71
|
with open('$MANIFEST_FILE') as f:
|
|
70
72
|
data = json.load(f)
|
|
@@ -77,10 +79,22 @@ try:
|
|
|
77
79
|
except Exception:
|
|
78
80
|
optionals = {}
|
|
79
81
|
|
|
82
|
+
try:
|
|
83
|
+
with open(schedule_file) as f:
|
|
84
|
+
schedule = json.load(f)
|
|
85
|
+
if isinstance(schedule, dict):
|
|
86
|
+
automation_default = bool(schedule.get('automation_enabled', True))
|
|
87
|
+
except Exception:
|
|
88
|
+
automation_default = True
|
|
89
|
+
|
|
80
90
|
for c in data.get('crons', []):
|
|
81
91
|
cid = c['id']
|
|
82
92
|
optional_key = c.get('optional')
|
|
83
|
-
if optional_key
|
|
93
|
+
if optional_key == 'automation':
|
|
94
|
+
optional_enabled = optionals.get(optional_key, automation_default)
|
|
95
|
+
else:
|
|
96
|
+
optional_enabled = optionals.get(optional_key, False)
|
|
97
|
+
if optional_key and not optional_enabled:
|
|
84
98
|
continue
|
|
85
99
|
name = cid.replace('-', ' ').title()
|
|
86
100
|
# Use the right service identifier per platform
|
|
@@ -1126,18 +1140,28 @@ CONSTRAINTS:
|
|
|
1126
1140
|
- Log what you did to $NEXO_HOME/logs/watchdog-repair-result.log
|
|
1127
1141
|
NEXOPROMPT
|
|
1128
1142
|
|
|
1129
|
-
# Launch NEXO in background with
|
|
1130
|
-
#
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1143
|
+
# Launch NEXO in background with the configured automation backend.
|
|
1144
|
+
# Keep the hardened Claude fallback for older runtimes or partial installs.
|
|
1145
|
+
AGENT_RUNNER="$NEXO_HOME/scripts/nexo-agent-run.py"
|
|
1146
|
+
NEXO_PYTHON="$NEXO_HOME/.venv/bin/python3"
|
|
1147
|
+
if [ ! -x "$NEXO_PYTHON" ]; then
|
|
1148
|
+
NEXO_PYTHON=$(command -v python3 2>/dev/null || echo "python3")
|
|
1134
1149
|
fi
|
|
1135
1150
|
|
|
1136
|
-
if [ -
|
|
1137
|
-
nohup bash -c "\"$
|
|
1151
|
+
if [ -f "$AGENT_RUNNER" ]; then
|
|
1152
|
+
nohup bash -c "\"$NEXO_PYTHON\" \"$AGENT_RUNNER\" --prompt-file '$REPAIR_PROMPT_FILE' >> '$LOG_DIR/watchdog-nexo-repair.log' 2>&1; rm -f '$REPAIR_PROMPT_FILE'" &
|
|
1138
1153
|
else
|
|
1139
|
-
|
|
1140
|
-
|
|
1154
|
+
CLAUDE_BIN=$(command -v claude 2>/dev/null || echo "$HOME_DIR/.claude/local/bin/claude")
|
|
1155
|
+
if [ ! -x "$CLAUDE_BIN" ]; then
|
|
1156
|
+
CLAUDE_BIN=$(find /usr/local/bin /opt/homebrew/bin "$HOME_DIR/.local/bin" "$HOME_DIR/.npm-global/bin" -name claude -type f 2>/dev/null | head -1)
|
|
1157
|
+
fi
|
|
1158
|
+
|
|
1159
|
+
if [ -n "$CLAUDE_BIN" ] && [ -x "$CLAUDE_BIN" ]; then
|
|
1160
|
+
nohup bash -c "\"$CLAUDE_BIN\" --print --dangerously-skip-permissions -p \"\$(cat '$REPAIR_PROMPT_FILE')\" >> '$LOG_DIR/watchdog-nexo-repair.log' 2>&1; rm -f '$REPAIR_PROMPT_FILE'" &
|
|
1161
|
+
else
|
|
1162
|
+
log "NEXO repair ABORTED: no automation backend wrapper and no claude CLI fallback found"
|
|
1163
|
+
rm -f "$REPAIR_PROMPT_FILE"
|
|
1164
|
+
fi
|
|
1141
1165
|
fi
|
|
1142
1166
|
|
|
1143
1167
|
REPAIR_PID=$!
|
package/templates/nexo_helper.py
CHANGED
|
@@ -9,7 +9,13 @@ All communication goes through the stable `nexo scripts call` CLI.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
import os
|
|
12
13
|
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
def run_nexo(args: list[str]) -> str:
|
|
@@ -43,3 +49,42 @@ def call_tool_json(name: str, payload: dict | None = None) -> dict:
|
|
|
43
49
|
args = ["scripts", "call", name, "--input", json.dumps(payload or {}), "--json-output"]
|
|
44
50
|
out = run_nexo(args)
|
|
45
51
|
return json.loads(out)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_automation_text(
|
|
55
|
+
prompt: str,
|
|
56
|
+
*,
|
|
57
|
+
model: str = "",
|
|
58
|
+
reasoning_effort: str = "",
|
|
59
|
+
cwd: str = "",
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Run the configured NEXO automation backend and return text output.
|
|
62
|
+
|
|
63
|
+
This avoids hardcoding provider CLIs such as `claude -p` inside personal
|
|
64
|
+
scripts. The runtime routes the call through the selected backend and its
|
|
65
|
+
configured model profile.
|
|
66
|
+
"""
|
|
67
|
+
runner = NEXO_HOME / "scripts" / "nexo-agent-run.py"
|
|
68
|
+
if not runner.exists():
|
|
69
|
+
raise RuntimeError(f"Automation runner not found: {runner}")
|
|
70
|
+
|
|
71
|
+
cmd = [sys.executable, str(runner), "--prompt", prompt, "--output-format", "text"]
|
|
72
|
+
if model:
|
|
73
|
+
cmd.extend(["--model", model])
|
|
74
|
+
if reasoning_effort:
|
|
75
|
+
cmd.extend(["--reasoning-effort", reasoning_effort])
|
|
76
|
+
if cwd:
|
|
77
|
+
cmd.extend(["--cwd", cwd])
|
|
78
|
+
|
|
79
|
+
env = os.environ.copy()
|
|
80
|
+
env.setdefault("NEXO_HOME", str(NEXO_HOME))
|
|
81
|
+
env.setdefault("NEXO_CODE", env.get("NEXO_CODE", str(NEXO_HOME)))
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
cmd,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
env=env,
|
|
87
|
+
)
|
|
88
|
+
if result.returncode != 0:
|
|
89
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"automation backend exited {result.returncode}")
|
|
90
|
+
return result.stdout
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
This file lives in NEXO_HOME/plugins/ and is loaded by the NEXO MCP server.
|
|
4
4
|
Edit the handler below to implement your personal capability.
|
|
5
|
+
|
|
6
|
+
If this plugin ever needs an autonomous model call, route it through the
|
|
7
|
+
configured NEXO automation backend instead of hardcoding `claude -p` or
|
|
8
|
+
provider-specific model names directly inside the plugin.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
from __future__ import annotations
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
This template demonstrates:
|
|
14
14
|
- Inline metadata for auto-discovery
|
|
15
15
|
- Safe CLI calls through nexo_helper
|
|
16
|
+
- Optional agent calls through the configured automation backend
|
|
16
17
|
- Timeout handling (via metadata)
|
|
17
18
|
- argparse for user arguments
|
|
18
19
|
- No direct DB access
|
|
@@ -25,11 +26,21 @@ import sys
|
|
|
25
26
|
# nexo_helper.py is in NEXO_HOME/templates/ — copy it next to your script
|
|
26
27
|
# or add the templates dir to your path
|
|
27
28
|
try:
|
|
28
|
-
from nexo_helper import call_tool_text
|
|
29
|
+
from nexo_helper import call_tool_text, run_automation_text
|
|
29
30
|
except ImportError:
|
|
30
31
|
import os
|
|
31
32
|
sys.path.insert(0, os.path.join(os.environ.get("NEXO_HOME", "~/.nexo"), "templates"))
|
|
32
|
-
from nexo_helper import call_tool_text
|
|
33
|
+
from nexo_helper import call_tool_text, run_automation_text
|
|
34
|
+
|
|
35
|
+
# If this script ever needs an autonomous model call:
|
|
36
|
+
# 1. use run_automation_text(...)
|
|
37
|
+
# 2. pass a legacy task profile like model="opus" when useful
|
|
38
|
+
# 3. DO NOT hardcode `claude -p` or provider-specific model defaults
|
|
39
|
+
# Example:
|
|
40
|
+
# result = run_automation_text(
|
|
41
|
+
# "Summarize pending issues",
|
|
42
|
+
# model="opus", # legacy task profile; NEXO maps it per backend
|
|
43
|
+
# )
|
|
33
44
|
|
|
34
45
|
|
|
35
46
|
def main():
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
This script is meant to be referenced by a Skill v2 definition.
|
|
5
5
|
It should use the stable NEXO CLI rather than importing internal DB modules.
|
|
6
|
+
If it needs an agentic model call, route it through NEXO's configured
|
|
7
|
+
automation backend instead of hardcoding `claude -p` or a provider model.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
import argparse
|
|
@@ -35,5 +37,11 @@ def main() -> int:
|
|
|
35
37
|
return result.returncode
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
# Agentic example for future edits:
|
|
41
|
+
# from nexo_helper import run_automation_text
|
|
42
|
+
# result = run_automation_text("Analyze this", model="opus")
|
|
43
|
+
# print(result)
|
|
44
|
+
|
|
45
|
+
|
|
38
46
|
if __name__ == "__main__":
|
|
39
47
|
raise SystemExit(main())
|