nexo-brain 2.4.0 → 2.5.0

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.
Files changed (80) hide show
  1. package/README.md +65 -2
  2. package/bin/nexo-brain.js +208 -11
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +5 -2
  6. package/src/auto_update.py +158 -8
  7. package/src/cli.py +605 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/hooks/post-compact.sh +5 -1
  54. package/src/hooks/pre-compact.sh +1 -1
  55. package/src/plugins/doctor.py +36 -0
  56. package/src/plugins/evolution.py +2 -1
  57. package/src/plugins/skills.py +135 -175
  58. package/src/requirements.txt +1 -0
  59. package/src/script_registry.py +322 -0
  60. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  61. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  62. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  63. package/src/scripts/deep-sleep/synthesize.py +37 -1
  64. package/src/scripts/nexo-dashboard.sh +29 -0
  65. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  66. package/src/scripts/nexo-evolution-run.py +2 -1
  67. package/src/scripts/nexo-learning-housekeep.py +1 -1
  68. package/src/scripts/nexo-watchdog.sh +1 -1
  69. package/src/server.py +9 -5
  70. package/src/skills/run-runtime-doctor/guide.md +12 -0
  71. package/src/skills/run-runtime-doctor/script.py +21 -0
  72. package/src/skills/run-runtime-doctor/skill.json +25 -0
  73. package/src/skills_runtime.py +347 -0
  74. package/src/tools_menu.py +3 -2
  75. package/src/tools_sessions.py +126 -0
  76. package/src/user_context.py +46 -0
  77. package/templates/nexo_helper.py +45 -0
  78. package/templates/script-template.py +44 -0
  79. package/templates/skill-script-template.py +39 -0
  80. package/templates/skill-template.md +33 -0
@@ -101,9 +101,74 @@ def handle_startup(task: str = "Startup", claude_session_id: str = "") -> str:
101
101
  age = _format_age(m["created_epoch"])
102
102
  lines.append(f" [{m['from_sid']}] ({age}): {m['text']}")
103
103
 
104
+ # Check LaunchAgent health (macOS only)
105
+ la_warnings = _check_launchagents()
106
+ if la_warnings:
107
+ lines.append("")
108
+ lines.append("⚠ LAUNCHAGENT MISMATCH (plist on disk ≠ loaded in memory):")
109
+ for w in la_warnings:
110
+ lines.append(f" {w}")
111
+ lines.append(" Fix: launchctl unload + load the affected plists, or restart.")
112
+
104
113
  return "\n".join(lines)
105
114
 
106
115
 
116
+ def _check_launchagents() -> list[str]:
117
+ """Compare on-disk plists with what launchctl has loaded. macOS only."""
118
+ import platform
119
+ if platform.system() != "Darwin":
120
+ return []
121
+
122
+ import os, subprocess, plistlib, glob
123
+
124
+ plist_dir = os.path.expanduser("~/Library/LaunchAgents")
125
+ warnings = []
126
+
127
+ for plist_path in glob.glob(os.path.join(plist_dir, "com.nexo.*.plist")):
128
+ label = os.path.basename(plist_path).replace(".plist", "")
129
+ try:
130
+ with open(plist_path, "rb") as f:
131
+ disk = plistlib.load(f)
132
+ disk_args = disk.get("ProgramArguments", [])
133
+
134
+ result = subprocess.run(
135
+ ["launchctl", "list", label],
136
+ capture_output=True, text=True, timeout=5
137
+ )
138
+ if result.returncode != 0:
139
+ warnings.append(f"{label}: not loaded (plist exists on disk)")
140
+ continue
141
+
142
+ # Parse loaded ProgramArguments from launchctl output
143
+ loaded_args = []
144
+ in_args = False
145
+ for line in result.stdout.splitlines():
146
+ if '"ProgramArguments"' in line:
147
+ in_args = True
148
+ continue
149
+ if in_args:
150
+ line = line.strip().rstrip(";")
151
+ if line == ");":
152
+ break
153
+ if line.startswith('"') and line.endswith('"'):
154
+ loaded_args.append(line.strip('"'))
155
+
156
+ if loaded_args and disk_args and loaded_args != disk_args:
157
+ # Check if loaded path points to /tmp or nonexistent path
158
+ stale = any("/tmp/" in a or not os.path.exists(a) for a in loaded_args if "/" in a)
159
+ if stale:
160
+ # Auto-repair: reload the plist
161
+ subprocess.run(["launchctl", "unload", plist_path], capture_output=True, timeout=5)
162
+ subprocess.run(["launchctl", "load", plist_path], capture_output=True, timeout=5)
163
+ warnings.append(f"{label}: AUTO-REPAIRED (was pointing to stale/tmp path, reloaded from disk)")
164
+ else:
165
+ warnings.append(f"{label}: loaded args differ from disk plist")
166
+ except Exception:
167
+ continue
168
+
169
+ return warnings
170
+
171
+
107
172
  def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
108
173
  """Update session, check inbox + questions. Lightweight — no embeddings, no RAG.
109
174
 
@@ -209,6 +274,18 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
209
274
  parts.append("")
210
275
  parts.append(f"⚠ DIARY_OVERDUE: {_hb_count} heartbeats, {int(age_seconds/60)}min active, no diary. Write nexo_session_diary_write NOW.")
211
276
 
277
+ # Guard check reminder: if context_hint mentions code editing and no guard_check this session
278
+ if context_hint and _hint_suggests_code_edit(context_hint):
279
+ try:
280
+ guard_used = conn.execute(
281
+ "SELECT COUNT(*) FROM guard_log WHERE session_id = ?", (sid,)
282
+ ).fetchone()[0]
283
+ if guard_used == 0:
284
+ parts.append("")
285
+ parts.append("⚠ GUARD REMINDER: You appear to be editing code but haven't called `nexo_guard_check` this session. Do it NOW before any edits.")
286
+ except Exception:
287
+ pass # guard_log table may not exist in older installs
288
+
212
289
  return "\n".join(parts)
213
290
 
214
291
 
@@ -445,11 +522,60 @@ def handle_smart_startup_query() -> str:
445
522
  lines.append("")
446
523
  lines.append(tone)
447
524
 
525
+ # Toolbox reminder: skills + behavioral learnings count
526
+ toolbox = _toolbox_summary(conn)
527
+ if toolbox:
528
+ lines.append("")
529
+ lines.append(toolbox)
530
+
448
531
  return "\n".join(lines)
449
532
  except Exception as e:
450
533
  return f"Smart startup query error: {e}"
451
534
 
452
535
 
536
+ def _hint_suggests_code_edit(hint: str) -> bool:
537
+ """Check if a heartbeat context_hint suggests the agent is editing code."""
538
+ hint_lower = hint.lower()
539
+ edit_signals = ['edit', 'fix', 'patch', 'modify', 'implement', 'refactor', 'add function',
540
+ 'change code', 'update script', 'write code', '.py', '.js', '.ts', '.php',
541
+ 'commit', 'arregl', 'modific', 'implement', 'correg']
542
+ return any(signal in hint_lower for signal in edit_signals)
543
+
544
+
545
+ def _toolbox_summary(conn) -> str:
546
+ """Quick count of available skills and behavioral learnings for startup reminder."""
547
+ try:
548
+ skill_count = conn.execute(
549
+ "SELECT COUNT(*) FROM skills"
550
+ ).fetchone()[0]
551
+ learning_count = conn.execute(
552
+ "SELECT COUNT(*) FROM learnings WHERE status = 'active' AND priority IN ('critical', 'high')"
553
+ ).fetchone()[0]
554
+ parts = []
555
+ if skill_count > 0:
556
+ parts.append(f"{skill_count} skills available — use `nexo_skill_match(task)` before multi-step tasks")
557
+ try:
558
+ from skills_runtime import get_featured_skill_summaries
559
+
560
+ featured = get_featured_skill_summaries(limit=3)
561
+ if featured:
562
+ parts.append("Featured skills:")
563
+ for skill in featured:
564
+ triggers = ", ".join(skill.get("trigger_patterns", [])[:2]) or "no triggers"
565
+ parts.append(
566
+ f"- {skill['id']} — {skill['mode']}/{skill['execution_level']} — triggers: {triggers}"
567
+ )
568
+ except Exception:
569
+ pass
570
+ if learning_count > 0:
571
+ parts.append(f"{learning_count} high-priority learnings — use `nexo_guard_check` before editing code")
572
+ if parts:
573
+ return "TOOLBOX REMINDER:\n " + "\n ".join(parts)
574
+ except Exception:
575
+ pass
576
+ return ""
577
+
578
+
453
579
  def handle_stop(sid: str) -> str:
454
580
  """Cleanly close a session, removing it from active sessions immediately."""
455
581
  _stop_keepalive(sid)
@@ -0,0 +1,46 @@
1
+ """User context singleton — loads operator/user identity from calibration.json."""
2
+ from __future__ import annotations
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ _ctx = None
8
+
9
+ class UserContext:
10
+ """Cached user/operator identity loaded once from calibration.json."""
11
+
12
+ def __init__(self):
13
+ nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
14
+ cal_path = nexo_home / "brain" / "calibration.json"
15
+ ver_path = nexo_home / "version.json"
16
+
17
+ self.assistant_name = "NEXO"
18
+ self.user_name = ""
19
+ self.user_language = "en"
20
+
21
+ # calibration.json has operator_name + user info
22
+ if cal_path.exists():
23
+ try:
24
+ cal = json.loads(cal_path.read_text())
25
+ self.assistant_name = cal.get("operator_name", "") or \
26
+ cal.get("user", {}).get("assistant_name", "") or "NEXO"
27
+ self.user_name = cal.get("user", {}).get("name", "")
28
+ self.user_language = cal.get("user", {}).get("language", "en")
29
+ except Exception:
30
+ pass
31
+
32
+ # Fallback: version.json also has operator_name
33
+ if self.assistant_name == "NEXO" and ver_path.exists():
34
+ try:
35
+ ver = json.loads(ver_path.read_text())
36
+ self.assistant_name = ver.get("operator_name", "") or "NEXO"
37
+ except Exception:
38
+ pass
39
+
40
+
41
+ def get_context() -> UserContext:
42
+ """Get or create the singleton UserContext."""
43
+ global _ctx
44
+ if _ctx is None:
45
+ _ctx = UserContext()
46
+ return _ctx
@@ -0,0 +1,45 @@
1
+ """NEXO Helper — vendorable utility for personal scripts.
2
+
3
+ Provides stable access to NEXO MCP tools via the CLI.
4
+ Copy this file next to your script or keep it in NEXO_HOME/templates/.
5
+
6
+ This module does NOT import any NEXO internals (db, server, cognitive).
7
+ All communication goes through the stable `nexo scripts call` CLI.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import subprocess
13
+
14
+
15
+ def run_nexo(args: list[str]) -> str:
16
+ """Run a nexo CLI command and return stdout.
17
+
18
+ Raises RuntimeError on non-zero exit.
19
+ """
20
+ result = subprocess.run(
21
+ ["nexo", *args],
22
+ capture_output=True,
23
+ text=True,
24
+ )
25
+ if result.returncode != 0:
26
+ raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"nexo exited {result.returncode}")
27
+ return result.stdout
28
+
29
+
30
+ def call_tool(name: str, payload: dict | None = None) -> str:
31
+ """Call a NEXO MCP tool by name. Returns raw text output."""
32
+ args = ["scripts", "call", name, "--input", json.dumps(payload or {})]
33
+ return run_nexo(args)
34
+
35
+
36
+ def call_tool_text(name: str, payload: dict | None = None) -> str:
37
+ """Call a NEXO MCP tool and return text output."""
38
+ return call_tool(name, payload)
39
+
40
+
41
+ def call_tool_json(name: str, payload: dict | None = None) -> dict:
42
+ """Call a NEXO MCP tool and return parsed JSON output."""
43
+ args = ["scripts", "call", name, "--input", json.dumps(payload or {}), "--json-output"]
44
+ out = run_nexo(args)
45
+ return json.loads(out)
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ # nexo: name=example-script
3
+ # nexo: description=Example personal script using the stable NEXO CLI
4
+ # nexo: runtime=python
5
+ # nexo: timeout=60
6
+ # nexo: tools=nexo_learning_search,nexo_schedule_status
7
+
8
+ """Example personal script for NEXO.
9
+
10
+ This template demonstrates:
11
+ - Inline metadata for auto-discovery
12
+ - Safe CLI calls through nexo_helper
13
+ - Timeout handling (via metadata)
14
+ - argparse for user arguments
15
+ - No direct DB access
16
+ - Clean exit codes
17
+ """
18
+
19
+ import argparse
20
+ import sys
21
+
22
+ # nexo_helper.py is in NEXO_HOME/templates/ — copy it next to your script
23
+ # or add the templates dir to your path
24
+ try:
25
+ from nexo_helper import call_tool_text
26
+ except ImportError:
27
+ import os
28
+ sys.path.insert(0, os.path.join(os.environ.get("NEXO_HOME", "~/.nexo"), "templates"))
29
+ from nexo_helper import call_tool_text
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(description="Example NEXO personal script")
34
+ parser.add_argument("--query", default="cron", help="Search query for learnings")
35
+ args = parser.parse_args()
36
+
37
+ print(f"Searching learnings for: {args.query}")
38
+ result = call_tool_text("nexo_learning_search", {"query": args.query})
39
+ print(result)
40
+ return 0
41
+
42
+
43
+ if __name__ == "__main__":
44
+ raise SystemExit(main())
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ """Skill script template.
3
+
4
+ This script is meant to be referenced by a Skill v2 definition.
5
+ It should use the stable NEXO CLI rather than importing internal DB modules.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import subprocess
11
+ import sys
12
+
13
+
14
+ def main() -> int:
15
+ parser = argparse.ArgumentParser()
16
+ parser.add_argument("--query", default="")
17
+ args = parser.parse_args()
18
+
19
+ nexo_code = os.environ.get("NEXO_CODE", "")
20
+ if not nexo_code:
21
+ print("NEXO_CODE not set", file=sys.stderr)
22
+ return 1
23
+
24
+ cli_py = os.path.join(nexo_code, "cli.py")
25
+ cmd = [
26
+ sys.executable,
27
+ cli_py,
28
+ "scripts",
29
+ "call",
30
+ "nexo_learning_search",
31
+ "--input",
32
+ '{"query": %r}' % args.query,
33
+ ]
34
+ result = subprocess.run(cmd, text=True)
35
+ return result.returncode
36
+
37
+
38
+ if __name__ == "__main__":
39
+ raise SystemExit(main())
@@ -0,0 +1,33 @@
1
+ # Skill Template
2
+
3
+ Create a directory under one of:
4
+ - `NEXO_HOME/skills/<slug>/`
5
+ - `src/skills/<slug>/`
6
+ - `community/skills/<slug>/`
7
+
8
+ Required files:
9
+ - `skill.json`
10
+ - `guide.md`
11
+
12
+ Optional:
13
+ - `script.py` or `script.sh`
14
+
15
+ Example `skill.json`:
16
+
17
+ ```json
18
+ {
19
+ "id": "SK-EXAMPLE",
20
+ "name": "Example Skill",
21
+ "description": "What this skill does.",
22
+ "level": "draft",
23
+ "mode": "guide",
24
+ "source_kind": "personal",
25
+ "execution_level": "none",
26
+ "approval_required": false,
27
+ "tags": ["example"],
28
+ "trigger_patterns": ["example task"],
29
+ "params_schema": {},
30
+ "command_template": {},
31
+ "stable_after_uses": 10
32
+ }
33
+ ```