nexo-brain 2.4.0 → 2.5.1

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 (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -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/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO — Cognitive co-operator for Claude Code. Memory, emotional intelligence, overnight learning (Deep Sleep), cron management, trust scoring, and adaptive calibration.",
6
6
  "bin": {
7
- "nexo-brain": "./bin/nexo-brain.js"
7
+ "nexo-brain": "./bin/nexo-brain.js",
8
+ "nexo": "./bin/nexo.js"
8
9
  },
9
10
  "keywords": [
10
11
  "claude-code",
@@ -38,7 +39,12 @@
38
39
  "cron-manifest",
39
40
  "session-tone",
40
41
  "mood-tracking",
41
- "productivity-analysis"
42
+ "productivity-analysis",
43
+ "runtime-cli",
44
+ "doctor",
45
+ "executable-skills",
46
+ "day-orchestrator",
47
+ "work-continuity"
42
48
  ],
43
49
  "author": "NEXO Brain <info@nexo-brain.com>",
44
50
  "license": "AGPL-3.0",
@@ -54,8 +60,10 @@
54
60
  },
55
61
  "files": [
56
62
  "bin/nexo-brain.js",
63
+ "bin/nexo.js",
57
64
  "bin/postinstall.js",
58
65
  "src/",
66
+ "community/",
59
67
  "!src/**/__pycache__",
60
68
  "!src/**/*.pyc",
61
69
  "!src/**/*.pyo",
@@ -8,6 +8,7 @@ This is separate from plugins/update.py which handles MANUAL updates with rollba
8
8
  """
9
9
 
10
10
  import json
11
+ import hashlib
11
12
  import os
12
13
  import re
13
14
  import subprocess
@@ -56,6 +57,30 @@ def _write_last_check(data: dict):
56
57
  _log(f"Failed to write last-check file: {e}")
57
58
 
58
59
 
60
+ def _sync_watchdog_hash_registry():
61
+ """Keep the immutable-hash registry aligned with the installed watchdog script."""
62
+ try:
63
+ watchdog_file = NEXO_HOME / "scripts" / "nexo-watchdog.sh"
64
+ if not watchdog_file.exists():
65
+ return
66
+ registry_file = NEXO_HOME / "scripts" / ".watchdog-hashes"
67
+ entries: dict[str, str] = {}
68
+ if registry_file.exists():
69
+ for line in registry_file.read_text().splitlines():
70
+ if "|" not in line:
71
+ continue
72
+ filepath, expected = line.split("|", 1)
73
+ if filepath:
74
+ entries[filepath] = expected
75
+ actual_hash = hashlib.sha256(watchdog_file.read_bytes()).hexdigest()
76
+ entries[str(watchdog_file)] = actual_hash
77
+ registry_file.write_text(
78
+ "\n".join(f"{filepath}|{digest}" for filepath, digest in sorted(entries.items())) + "\n"
79
+ )
80
+ except Exception as e:
81
+ _log(f"watchdog hash registry sync error: {e}")
82
+
83
+
59
84
  def _is_git_repo() -> bool:
60
85
  """Check if REPO_DIR is inside a git repository."""
61
86
  try:
@@ -94,6 +119,61 @@ def _read_package_version() -> str:
94
119
  return "unknown"
95
120
 
96
121
 
122
+ def _runtime_cli_wrapper_text() -> str:
123
+ return (
124
+ "#!/usr/bin/env bash\n"
125
+ "set -euo pipefail\n\n"
126
+ f'NEXO_HOME="{NEXO_HOME}"\n'
127
+ 'PYTHON="$NEXO_HOME/.venv/bin/python3"\n'
128
+ 'if [ ! -x "$PYTHON" ]; then\n'
129
+ ' if command -v python3 >/dev/null 2>&1; then\n'
130
+ ' PYTHON="python3"\n'
131
+ " else\n"
132
+ ' PYTHON="python"\n'
133
+ " fi\n"
134
+ "fi\n"
135
+ 'export NEXO_HOME\n'
136
+ 'export NEXO_CODE="$NEXO_HOME"\n'
137
+ 'exec "$PYTHON" "$NEXO_HOME/cli.py" "$@"\n'
138
+ )
139
+
140
+
141
+ def _shell_rc_files() -> list[Path]:
142
+ shell = os.environ.get("SHELL", "/bin/bash")
143
+ home_dir = Path.home()
144
+ if "zsh" in shell:
145
+ return [home_dir / ".zshrc"]
146
+ return [home_dir / ".bash_profile", home_dir / ".bashrc"]
147
+
148
+
149
+ def _ensure_runtime_cli_in_shell():
150
+ path_line = f'export PATH="{NEXO_HOME / "bin"}:$PATH"'
151
+ comment = "# NEXO runtime CLI"
152
+ for rc_file in _shell_rc_files():
153
+ try:
154
+ content = rc_file.read_text() if rc_file.exists() else ""
155
+ if path_line not in content:
156
+ with rc_file.open("a") as fh:
157
+ fh.write(f"\n{comment}\n{path_line}\n")
158
+ _log(f"Backfilled runtime CLI PATH in {rc_file.name}")
159
+ except Exception as e:
160
+ _log(f"Shell PATH backfill error for {rc_file.name}: {e}")
161
+
162
+
163
+ def _ensure_runtime_cli_wrapper():
164
+ try:
165
+ bin_dir = NEXO_HOME / "bin"
166
+ bin_dir.mkdir(parents=True, exist_ok=True)
167
+ wrapper = bin_dir / "nexo"
168
+ content = _runtime_cli_wrapper_text()
169
+ if not wrapper.exists() or wrapper.read_text() != content:
170
+ wrapper.write_text(content)
171
+ wrapper.chmod(0o755)
172
+ _log("Backfilled runtime CLI wrapper")
173
+ except Exception as e:
174
+ _log(f"Runtime CLI wrapper backfill error: {e}")
175
+
176
+
97
177
  # ── Hook sync ────────────────────────────────────────────────────────
98
178
 
99
179
  def _requirements_hash() -> str:
@@ -402,7 +482,12 @@ def _check_npm_version() -> str | None:
402
482
  if not latest:
403
483
  return None
404
484
  if latest != current and not current.endswith(latest):
405
- return f"NEXO update available: {current} -> {latest}. Run: npm update -g {pkg_name}"
485
+ try:
486
+ from user_context import get_context
487
+ _name = get_context().assistant_name
488
+ except Exception:
489
+ _name = "NEXO"
490
+ return f"{_name} update available: {current} -> {latest}. Run: npm update -g {pkg_name}"
406
491
  except Exception:
407
492
  pass
408
493
  return None
@@ -577,15 +662,12 @@ def _find_user_claude_md() -> Path | None:
577
662
 
578
663
  def _resolve_placeholders(template_text: str) -> str:
579
664
  """Fill {{NAME}} and {{NEXO_HOME}} from the user's existing CLAUDE.md or config."""
580
- # Try to read operator name from version.json
581
- name = "NEXO"
665
+ # Read operator name from calibration/version
582
666
  try:
583
- vf = NEXO_HOME / "version.json"
584
- if vf.exists():
585
- data = json.loads(vf.read_text())
586
- name = data.get("operator_name", name)
667
+ from user_context import get_context
668
+ name = get_context().assistant_name
587
669
  except Exception:
588
- pass
670
+ name = "NEXO"
589
671
 
590
672
  return (
591
673
  template_text
@@ -754,13 +836,14 @@ def auto_update_check() -> dict:
754
836
  # Backfill evolution-objective.json for existing installs
755
837
  try:
756
838
  evo_obj_path = NEXO_HOME / "brain" / "evolution-objective.json"
839
+ from evolution_cycle import normalize_objective
757
840
  if not evo_obj_path.exists():
758
841
  (NEXO_HOME / "brain").mkdir(parents=True, exist_ok=True)
759
842
  default_objective = {
760
843
  "objective": "Improve operational excellence and reduce repeated errors",
761
844
  "focus_areas": ["error_prevention", "proactivity", "memory_quality"],
762
845
  "evolution_enabled": True,
763
- "evolution_mode": "review",
846
+ "evolution_mode": "auto",
764
847
  "dimensions": {
765
848
  "episodic_memory": {"current": 0, "target": 90},
766
849
  "autonomy": {"current": 0, "target": 80},
@@ -774,6 +857,12 @@ def auto_update_check() -> dict:
774
857
  }
775
858
  evo_obj_path.write_text(json.dumps(default_objective, indent=2))
776
859
  _log("Backfilled evolution-objective.json for existing install")
860
+ else:
861
+ raw_objective = json.loads(evo_obj_path.read_text())
862
+ normalized = normalize_objective(raw_objective)
863
+ if normalized != raw_objective:
864
+ evo_obj_path.write_text(json.dumps(normalized, indent=2, ensure_ascii=False))
865
+ _log("Normalized legacy evolution-objective.json")
777
866
  except Exception as e:
778
867
  _log(f"evolution-objective.json backfill error: {e}")
779
868
 
@@ -796,6 +885,101 @@ def auto_update_check() -> dict:
796
885
  except Exception as e:
797
886
  _log(f"scripts backfill error: {e}")
798
887
 
888
+ _sync_watchdog_hash_registry()
889
+
890
+ # Backfill runtime CLI modules for existing installs
891
+ try:
892
+ for fname in ("cli.py", "script_registry.py", "skills_runtime.py"):
893
+ src_file = SRC_DIR / fname
894
+ dest_file = NEXO_HOME / fname
895
+ if src_file.is_file() and (not dest_file.exists() or src_file.stat().st_mtime > dest_file.stat().st_mtime):
896
+ import shutil
897
+ shutil.copy2(str(src_file), str(dest_file))
898
+ _log(f"Backfilled {fname}")
899
+ except Exception as e:
900
+ _log(f"CLI backfill error: {e}")
901
+
902
+ _ensure_runtime_cli_wrapper()
903
+ _ensure_runtime_cli_in_shell()
904
+
905
+ # Backfill doctor package for existing installs
906
+ try:
907
+ doctor_src = SRC_DIR / "doctor"
908
+ doctor_dest = NEXO_HOME / "doctor"
909
+ if doctor_src.is_dir():
910
+ import shutil
911
+ if not doctor_dest.is_dir():
912
+ shutil.copytree(str(doctor_src), str(doctor_dest), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
913
+ _log("Backfilled doctor package")
914
+ else:
915
+ # Update existing files
916
+ for root, dirs, files in os.walk(str(doctor_src)):
917
+ dirs[:] = [d for d in dirs if d != "__pycache__"]
918
+ rel = os.path.relpath(root, str(doctor_src))
919
+ dest_dir = doctor_dest / rel
920
+ dest_dir.mkdir(parents=True, exist_ok=True)
921
+ for f in files:
922
+ if f.endswith(".pyc"):
923
+ continue
924
+ src_f = Path(root) / f
925
+ dst_f = dest_dir / f
926
+ if not dst_f.exists() or src_f.stat().st_mtime > dst_f.stat().st_mtime:
927
+ shutil.copy2(str(src_f), str(dst_f))
928
+ except Exception as e:
929
+ _log(f"Doctor backfill error: {e}")
930
+
931
+ # Backfill packaged core skills to a dedicated directory.
932
+ try:
933
+ skills_src = SRC_DIR / "skills"
934
+ skills_dest = NEXO_HOME / "skills-core"
935
+ if skills_src.is_dir():
936
+ import shutil
937
+ if not skills_dest.is_dir():
938
+ shutil.copytree(str(skills_src), str(skills_dest), ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
939
+ _log("Backfilled skills-core")
940
+ else:
941
+ for root, dirs, files in os.walk(str(skills_src)):
942
+ dirs[:] = [d for d in dirs if d != "__pycache__"]
943
+ rel = os.path.relpath(root, str(skills_src))
944
+ dest_dir = skills_dest / rel
945
+ dest_dir.mkdir(parents=True, exist_ok=True)
946
+ for f in files:
947
+ if f.endswith(".pyc"):
948
+ continue
949
+ src_f = Path(root) / f
950
+ dst_f = dest_dir / f
951
+ if not dst_f.exists() or src_f.stat().st_mtime > dst_f.stat().st_mtime:
952
+ shutil.copy2(str(src_f), str(dst_f))
953
+ except Exception as e:
954
+ _log(f"Skills backfill error: {e}")
955
+
956
+ # Backfill MCP doctor plugin so existing installs expose nexo_doctor.
957
+ try:
958
+ plugin_src = SRC_DIR / "plugins" / "doctor.py"
959
+ plugin_dest = NEXO_HOME / "plugins" / "doctor.py"
960
+ plugin_dest.parent.mkdir(parents=True, exist_ok=True)
961
+ if plugin_src.is_file() and (not plugin_dest.exists() or plugin_src.stat().st_mtime > plugin_dest.stat().st_mtime):
962
+ import shutil
963
+ shutil.copy2(str(plugin_src), str(plugin_dest))
964
+ _log("Backfilled doctor plugin")
965
+ except Exception as e:
966
+ _log(f"Doctor plugin backfill error: {e}")
967
+
968
+ # Backfill script/skill templates for existing installs
969
+ try:
970
+ templates_src = REPO_DIR / "templates"
971
+ templates_dest = NEXO_HOME / "templates"
972
+ templates_dest.mkdir(parents=True, exist_ok=True)
973
+ for fname in ("script-template.py", "nexo_helper.py", "skill-template.md", "skill-script-template.py"):
974
+ src_file = templates_src / fname
975
+ dest_file = templates_dest / fname
976
+ if src_file.is_file() and (not dest_file.exists() or src_file.stat().st_mtime > dest_file.stat().st_mtime):
977
+ import shutil
978
+ shutil.copy2(str(src_file), str(dest_file))
979
+ _log(f"Backfilled template {fname}")
980
+ except Exception as e:
981
+ _log(f"Template backfill error: {e}")
982
+
799
983
  # CLAUDE.md version migration
800
984
  try:
801
985
  result["claude_md_update"] = _migrate_claude_md()