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.
- package/README.md +65 -2
- package/bin/nexo-brain.js +208 -11
- package/bin/nexo.js +55 -0
- package/community/skills/.gitkeep +1 -0
- package/package.json +5 -2
- package/src/auto_update.py +158 -8
- package/src/cli.py +605 -0
- package/src/cognitive/_ingest.py +1 -1
- package/src/cognitive/_memory.py +4 -4
- package/src/crons/manifest.json +8 -0
- package/src/dashboard/app.py +700 -35
- package/src/dashboard/templates/adaptive.html +112 -218
- package/src/dashboard/templates/artifacts.html +133 -0
- package/src/dashboard/templates/backups.html +136 -0
- package/src/dashboard/templates/base.html +413 -0
- package/src/dashboard/templates/calendar.html +523 -654
- package/src/dashboard/templates/chat.html +356 -0
- package/src/dashboard/templates/claims.html +259 -0
- package/src/dashboard/templates/cortex.html +262 -0
- package/src/dashboard/templates/credentials.html +128 -0
- package/src/dashboard/templates/crons.html +370 -0
- package/src/dashboard/templates/dashboard.html +383 -578
- package/src/dashboard/templates/dreams.html +252 -0
- package/src/dashboard/templates/email.html +160 -0
- package/src/dashboard/templates/evolution.html +189 -0
- package/src/dashboard/templates/feed.html +249 -0
- package/src/dashboard/templates/followup_health.html +170 -0
- package/src/dashboard/templates/graph.html +191 -269
- package/src/dashboard/templates/guard.html +259 -0
- package/src/dashboard/templates/inbox.html +220 -346
- package/src/dashboard/templates/memory.html +317 -197
- package/src/dashboard/templates/operations.html +521 -698
- package/src/dashboard/templates/plugins.html +185 -0
- package/src/dashboard/templates/rules.html +246 -0
- package/src/dashboard/templates/sentiment.html +247 -0
- package/src/dashboard/templates/sessions.html +215 -182
- package/src/dashboard/templates/skills.html +329 -0
- package/src/dashboard/templates/somatic.html +68 -172
- package/src/dashboard/templates/triggers.html +133 -0
- package/src/dashboard/templates/trust.html +360 -0
- package/src/db/__init__.py +5 -0
- package/src/db/_schema.py +16 -1
- package/src/db/_sessions.py +22 -0
- package/src/db/_skills.py +980 -274
- package/src/doctor/__init__.py +1 -0
- package/src/doctor/formatters.py +52 -0
- package/src/doctor/models.py +44 -0
- package/src/doctor/orchestrator.py +42 -0
- package/src/doctor/providers/__init__.py +1 -0
- package/src/doctor/providers/boot.py +206 -0
- package/src/doctor/providers/deep.py +292 -0
- package/src/doctor/providers/runtime.py +686 -0
- package/src/hooks/post-compact.sh +5 -1
- package/src/hooks/pre-compact.sh +1 -1
- package/src/plugins/doctor.py +36 -0
- package/src/plugins/evolution.py +2 -1
- package/src/plugins/skills.py +135 -175
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +322 -0
- package/src/scripts/deep-sleep/apply_findings.py +63 -48
- package/src/scripts/deep-sleep/extract-prompt.md +14 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
- package/src/scripts/deep-sleep/synthesize.py +37 -1
- package/src/scripts/nexo-dashboard.sh +29 -0
- package/src/scripts/nexo-day-orchestrator.sh +139 -0
- package/src/scripts/nexo-evolution-run.py +2 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -1
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -5
- package/src/skills/run-runtime-doctor/guide.md +12 -0
- package/src/skills/run-runtime-doctor/script.py +21 -0
- package/src/skills/run-runtime-doctor/skill.json +25 -0
- package/src/skills_runtime.py +347 -0
- package/src/tools_menu.py +3 -2
- package/src/tools_sessions.py +126 -0
- package/src/user_context.py +46 -0
- package/templates/nexo_helper.py +45 -0
- package/templates/script-template.py +44 -0
- package/templates/skill-script-template.py +39 -0
- package/templates/skill-template.md +33 -0
package/src/tools_sessions.py
CHANGED
|
@@ -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
|
+
```
|