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.
@@ -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
- Claude CLI (opus) to understand, deduplicate, and prune with real intelligence.
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 (Claude CLI opus):
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 (Claude CLI) ─────────────────────────────────────────
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 Claude CLI (opus) — dreaming...")
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 = subprocess.run(
439
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
440
- "--output-format", "text",
441
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
442
- capture_output=True, text=True, timeout=21600, env=env
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 Claude CLI (sonnet) which synthesizes
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 Claude CLI (opus) for synthesis...")
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 = subprocess.run(
215
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
216
- "--output-format", "text",
217
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
218
- capture_output=True, text=True, timeout=21600, env=env
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 Claude Code, Claude Desktop, and Codex configs..."
254
- if NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 "$CLIENT_SYNC" --nexo-home "$NEXO_HOME" --runtime-root "$SRC_DIR" --json >/dev/null 2>&1; then
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 and not optionals.get(optional_key, False):
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 repair task
1130
- # Ensure claude CLI is in PATH (cron/LaunchAgent may have minimal PATH)
1131
- CLAUDE_BIN=$(command -v claude 2>/dev/null || echo "$HOME_DIR/.claude/local/bin/claude")
1132
- if [ ! -x "$CLAUDE_BIN" ]; then
1133
- 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)
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 [ -n "$CLAUDE_BIN" ] && [ -x "$CLAUDE_BIN" ]; then
1137
- 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'" &
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
- log "NEXO repair ABORTED: claude CLI not found in PATH"
1140
- rm -f "$REPAIR_PROMPT_FILE"
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=$!
@@ -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())