nexo-brain 1.4.0 → 1.4.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.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
- package/src/__pycache__/db.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/cognitive.py +63 -14
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/dashboard/app.py +1 -1
- package/src/db.py +29 -13
- package/src/evolution_cycle.py +72 -94
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/hooks/session-start.sh +5 -2
- package/src/hooks/session-stop.sh +79 -133
- package/src/knowledge_graph.py +3 -3
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/plugins/artifact_registry.py +450 -0
- package/src/plugins/cognitive_memory.py +9 -9
- package/src/plugins/episodic_memory.py +8 -8
- package/src/plugins/evolution.py +4 -4
- package/src/plugins/guard.py +26 -2
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/nexo-brain-activation.sh +140 -0
- package/src/scripts/nexo-evolution-run.py +1 -1
- package/src/scripts/nexo-followup-hygiene.py +107 -0
- package/src/scripts/nexo-postmortem-consolidator.py +25 -25
- package/src/scripts/nexo-pre-commit.py +118 -0
- package/src/scripts/nexo-proactive-dashboard.py +342 -0
- package/src/scripts/nexo-runtime-preflight.py +270 -0
- package/src/scripts/nexo-send-email.py +25 -0
- package/src/scripts/nexo-send-reply.py +176 -0
- package/src/scripts/nexo-snapshot-restore.sh +34 -0
- package/src/scripts/nexo-watchdog-smoke.py +114 -0
- package/src/server.py +5 -5
- package/src/tools_coordination.py +3 -3
- package/src/tools_learnings.py +1 -1
- package/src/tools_menu.py +31 -49
- package/src/tools_reminders_crud.py +1 -1
- package/src/tools_sessions.py +12 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/migrate 2.py +0 -207
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Email Reply Helper — Sends email replies with correct threading headers.
|
|
4
|
+
NEXO calls this instead of building SMTP manually.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
nexo-send-reply.py --to addr --subject "Re: ..." --in-reply-to "<msg-id>" --body "text"
|
|
8
|
+
nexo-send-reply.py --to addr --subject "Re: ..." --in-reply-to "<msg-id>" --body-file /tmp/reply.html --html
|
|
9
|
+
nexo-send-reply.py --to addr --subject "New subject" --body "text" (new email, no threading)
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--to Recipient (required)
|
|
13
|
+
--cc CC recipients (comma-separated, default: info@example.com)
|
|
14
|
+
--subject Subject line (required)
|
|
15
|
+
--in-reply-to Message-ID of the email being replied to (for threading)
|
|
16
|
+
--references Full References chain (optional, defaults to in-reply-to value)
|
|
17
|
+
--body Plain text body (inline)
|
|
18
|
+
--body-file Read body from file instead
|
|
19
|
+
--html Treat body as HTML
|
|
20
|
+
--quote Original message text to include as quoted reply
|
|
21
|
+
--quote-file Read original message from file to quote
|
|
22
|
+
--quote-from Sender of the original message (for "On date, X wrote:")
|
|
23
|
+
--quote-date Date of the original message
|
|
24
|
+
--attachment Path to file attachment (can be repeated)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import imaplib
|
|
29
|
+
import json
|
|
30
|
+
import smtplib
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
from email.message import EmailMessage
|
|
34
|
+
from email.utils import make_msgid, formatdate
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
import mimetypes
|
|
37
|
+
|
|
38
|
+
CONFIG_PATH = Path.home() / ".nexo" / "nexo-email" / "config.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_config():
|
|
42
|
+
with open(CONFIG_PATH) as f:
|
|
43
|
+
return json.load(f)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_message(args, config):
|
|
47
|
+
msg = EmailMessage()
|
|
48
|
+
|
|
49
|
+
# From / To / CC
|
|
50
|
+
msg["From"] = f"NEXO <{config['email']}>"
|
|
51
|
+
msg["To"] = args.to
|
|
52
|
+
if args.cc:
|
|
53
|
+
msg["Cc"] = args.cc
|
|
54
|
+
|
|
55
|
+
# Subject
|
|
56
|
+
msg["Subject"] = args.subject
|
|
57
|
+
|
|
58
|
+
# Threading headers — this is the whole point of this script
|
|
59
|
+
if args.in_reply_to:
|
|
60
|
+
msg["In-Reply-To"] = args.in_reply_to
|
|
61
|
+
msg["References"] = args.references or args.in_reply_to
|
|
62
|
+
|
|
63
|
+
# Standard headers
|
|
64
|
+
msg["Message-ID"] = make_msgid(domain="example.com")
|
|
65
|
+
msg["Date"] = formatdate(localtime=True)
|
|
66
|
+
|
|
67
|
+
# Body
|
|
68
|
+
if args.body_file:
|
|
69
|
+
body = Path(args.body_file).read_text(encoding="utf-8")
|
|
70
|
+
else:
|
|
71
|
+
body = args.body or ""
|
|
72
|
+
|
|
73
|
+
# Quoted original message
|
|
74
|
+
quote_text = ""
|
|
75
|
+
if args.quote_file:
|
|
76
|
+
quote_text = Path(args.quote_file).read_text(encoding="utf-8")
|
|
77
|
+
elif args.quote:
|
|
78
|
+
quote_text = args.quote
|
|
79
|
+
|
|
80
|
+
if quote_text:
|
|
81
|
+
quote_from = args.quote_from or args.to
|
|
82
|
+
quote_date = args.quote_date or ""
|
|
83
|
+
attribution = f"El {quote_date}, {quote_from} escribió:" if quote_date else f"{quote_from} escribió:"
|
|
84
|
+
|
|
85
|
+
if args.html:
|
|
86
|
+
quoted_html = quote_text.replace("\n", "<br>") if "<" not in quote_text else quote_text
|
|
87
|
+
body = (
|
|
88
|
+
f"{body}<br><br>"
|
|
89
|
+
f"<div style=\"color:#666;\">{attribution}</div>"
|
|
90
|
+
f"<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;color:#666;\">"
|
|
91
|
+
f"{quoted_html}"
|
|
92
|
+
f"</blockquote>"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
quoted_lines = "\n".join(f"> {line}" for line in quote_text.splitlines())
|
|
96
|
+
body = f"{body}\n\n{attribution}\n{quoted_lines}"
|
|
97
|
+
|
|
98
|
+
if args.html:
|
|
99
|
+
msg.set_content(body, subtype="html")
|
|
100
|
+
else:
|
|
101
|
+
msg.set_content(body)
|
|
102
|
+
|
|
103
|
+
# Attachments
|
|
104
|
+
if args.attachment:
|
|
105
|
+
for filepath in args.attachment:
|
|
106
|
+
p = Path(filepath)
|
|
107
|
+
if not p.exists():
|
|
108
|
+
print(f"WARNING: attachment not found: {filepath}", file=sys.stderr)
|
|
109
|
+
continue
|
|
110
|
+
mime_type, _ = mimetypes.guess_type(str(p))
|
|
111
|
+
if mime_type is None:
|
|
112
|
+
mime_type = "application/octet-stream"
|
|
113
|
+
maintype, subtype = mime_type.split("/", 1)
|
|
114
|
+
with open(p, "rb") as f:
|
|
115
|
+
msg.add_attachment(
|
|
116
|
+
f.read(),
|
|
117
|
+
maintype=maintype,
|
|
118
|
+
subtype=subtype,
|
|
119
|
+
filename=p.name
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return msg
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def send(msg, config):
|
|
126
|
+
with smtplib.SMTP_SSL(config["smtp_host"], config["smtp_port"]) as server:
|
|
127
|
+
server.login(config["email"], config["password"])
|
|
128
|
+
server.send_message(msg)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def save_to_sent(msg, config):
|
|
132
|
+
"""Append the sent message to the IMAP Sent folder."""
|
|
133
|
+
try:
|
|
134
|
+
with imaplib.IMAP4_SSL(config["imap_host"], config["imap_port"]) as imap:
|
|
135
|
+
imap.login(config["email"], config["password"])
|
|
136
|
+
imap.append(
|
|
137
|
+
"INBOX.Sent",
|
|
138
|
+
r"\Seen",
|
|
139
|
+
imaplib.Time2Internaldate(time.time()),
|
|
140
|
+
msg.as_bytes(),
|
|
141
|
+
)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"WARNING: could not save to Sent folder: {e}", file=sys.stderr)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main():
|
|
147
|
+
parser = argparse.ArgumentParser(description="NEXO Email Reply Helper")
|
|
148
|
+
parser.add_argument("--to", required=True, help="Recipient email")
|
|
149
|
+
parser.add_argument("--cc", default=None, help="CC recipients (comma-separated, no default)")
|
|
150
|
+
parser.add_argument("--subject", required=True, help="Subject line")
|
|
151
|
+
parser.add_argument("--in-reply-to", default=None, help="Message-ID for threading")
|
|
152
|
+
parser.add_argument("--references", default=None, help="References chain for threading")
|
|
153
|
+
parser.add_argument("--body", default=None, help="Body text inline")
|
|
154
|
+
parser.add_argument("--body-file", default=None, help="Read body from file")
|
|
155
|
+
parser.add_argument("--html", action="store_true", help="Body is HTML")
|
|
156
|
+
parser.add_argument("--quote", default=None, help="Original message text to quote inline")
|
|
157
|
+
parser.add_argument("--quote-file", default=None, help="Read original message from file to quote")
|
|
158
|
+
parser.add_argument("--quote-from", default=None, help="Sender of original (for attribution line)")
|
|
159
|
+
parser.add_argument("--quote-date", default=None, help="Date of original message")
|
|
160
|
+
parser.add_argument("--attachment", action="append", help="File to attach (repeatable)")
|
|
161
|
+
args = parser.parse_args()
|
|
162
|
+
|
|
163
|
+
if not args.body and not args.body_file:
|
|
164
|
+
print("ERROR: --body or --body-file required", file=sys.stderr)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
config = load_config()
|
|
168
|
+
msg = build_message(args, config)
|
|
169
|
+
send(msg, config)
|
|
170
|
+
save_to_sent(msg, config)
|
|
171
|
+
|
|
172
|
+
print(f"OK: sent to {args.to} | subject: {args.subject} | threaded: {bool(args.in_reply_to)}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO Snapshot Restore — restores files from a snapshot directory.
|
|
3
|
+
# Usage: nexo-snapshot-restore.sh <snapshot-dir>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SNAP_DIR="${1:?Usage: nexo-snapshot-restore.sh <snapshot-dir>}"
|
|
7
|
+
MANIFEST="$SNAP_DIR/manifest.json"
|
|
8
|
+
RESTORE_LOG="$HOME/claude/logs/snapshot-restores.log"
|
|
9
|
+
|
|
10
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
11
|
+
echo "ERROR: No manifest.json in $SNAP_DIR" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
TS=$(date "+%Y-%m-%d %H:%M:%S")
|
|
16
|
+
mkdir -p "$(dirname "$RESTORE_LOG")"
|
|
17
|
+
echo "[$TS] Restoring from $SNAP_DIR" >> "$RESTORE_LOG"
|
|
18
|
+
|
|
19
|
+
python3 -c "
|
|
20
|
+
import json, shutil, os
|
|
21
|
+
manifest = json.load(open('$MANIFEST'))
|
|
22
|
+
for rel_path in manifest.get('files', []):
|
|
23
|
+
src = os.path.join('$SNAP_DIR', 'files', rel_path)
|
|
24
|
+
dst = os.path.expanduser('~/' + rel_path)
|
|
25
|
+
if os.path.exists(src):
|
|
26
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
27
|
+
shutil.copy2(src, dst)
|
|
28
|
+
print(f' Restored: {rel_path}')
|
|
29
|
+
else:
|
|
30
|
+
print(f' SKIP (not in snapshot): {rel_path}')
|
|
31
|
+
print('Restore complete.')
|
|
32
|
+
"
|
|
33
|
+
|
|
34
|
+
echo "[$TS] Restore complete from $SNAP_DIR" >> "$RESTORE_LOG"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Watchdog Smoke
|
|
4
|
+
|
|
5
|
+
Runs the same health checks as the shell watchdog, but never restores files,
|
|
6
|
+
restarts services, disables evolution or notifies the user.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
import subprocess
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
HOME = Path.home()
|
|
19
|
+
CLAUDE_DIR = HOME / ".nexo"
|
|
20
|
+
NEXO_DIR = CLAUDE_DIR / "nexo-mcp"
|
|
21
|
+
CORTEX_DIR = CLAUDE_DIR / "cortex"
|
|
22
|
+
LOG_DIR = CLAUDE_DIR / "logs"
|
|
23
|
+
SUMMARY_FILE = LOG_DIR / "watchdog-smoke-summary.json"
|
|
24
|
+
HASH_REGISTRY = CLAUDE_DIR / "scripts" / ".watchdog-hashes"
|
|
25
|
+
RESTORE_LOG = LOG_DIR / "snapshot-restores.log"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_restore_count_for_current_hour() -> int:
|
|
29
|
+
if not RESTORE_LOG.exists():
|
|
30
|
+
return 0
|
|
31
|
+
needle = datetime.now().strftime("%Y-%m-%d %H")
|
|
32
|
+
return sum(1 for line in RESTORE_LOG.read_text(errors="ignore").splitlines()
|
|
33
|
+
if needle in line and "/.codex/memories/nexo-" not in line)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sha256(path: Path) -> str:
|
|
37
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> int:
|
|
41
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
findings = []
|
|
43
|
+
|
|
44
|
+
db_path = NEXO_DIR / "nexo.db"
|
|
45
|
+
integrity = "missing"
|
|
46
|
+
if db_path.exists():
|
|
47
|
+
try:
|
|
48
|
+
conn = sqlite3.connect(str(db_path), timeout=10)
|
|
49
|
+
integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
|
|
50
|
+
conn.close()
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
integrity = f"error:{exc}"
|
|
53
|
+
if integrity != "ok":
|
|
54
|
+
findings.append({"severity": "ERROR", "area": "sqlite", "msg": f"integrity={integrity}"})
|
|
55
|
+
|
|
56
|
+
cortex_running = subprocess.run(
|
|
57
|
+
["pgrep", "-f", "cortex-wrapper.py"],
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
).returncode == 0
|
|
61
|
+
if not cortex_running:
|
|
62
|
+
findings.append({"severity": "WARN", "area": "cortex", "msg": "cortex-wrapper.py not running"})
|
|
63
|
+
|
|
64
|
+
backups = sorted((NEXO_DIR / "backups").glob("nexo-*.db"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
65
|
+
if backups:
|
|
66
|
+
age_seconds = int(datetime.now().timestamp() - backups[0].stat().st_mtime)
|
|
67
|
+
if age_seconds > 7200:
|
|
68
|
+
findings.append({"severity": "WARN", "area": "backup", "msg": f"latest backup age={age_seconds}s"})
|
|
69
|
+
else:
|
|
70
|
+
findings.append({"severity": "WARN", "area": "backup", "msg": "no backups found"})
|
|
71
|
+
|
|
72
|
+
if HASH_REGISTRY.exists():
|
|
73
|
+
for line in HASH_REGISTRY.read_text().splitlines():
|
|
74
|
+
if not line.strip():
|
|
75
|
+
continue
|
|
76
|
+
filepath, expected_hash = line.split("|", 1)
|
|
77
|
+
path = Path(filepath)
|
|
78
|
+
if path.exists() and _sha256(path) != expected_hash:
|
|
79
|
+
findings.append({"severity": "ERROR", "area": "immutable", "msg": f"hash mismatch: {filepath}"})
|
|
80
|
+
|
|
81
|
+
restore_count = _read_restore_count_for_current_hour()
|
|
82
|
+
if restore_count > 2:
|
|
83
|
+
findings.append({"severity": "ERROR", "area": "restore_loop", "msg": f"{restore_count} restores this hour"})
|
|
84
|
+
elif restore_count > 0:
|
|
85
|
+
findings.append({"severity": "INFO", "area": "restore_activity", "msg": f"{restore_count} restores this hour"})
|
|
86
|
+
|
|
87
|
+
objective = CORTEX_DIR / "evolution-objective.json"
|
|
88
|
+
evolution_enabled = None
|
|
89
|
+
if objective.exists():
|
|
90
|
+
obj = json.loads(objective.read_text())
|
|
91
|
+
evolution_enabled = obj.get("evolution_enabled", True)
|
|
92
|
+
if not evolution_enabled:
|
|
93
|
+
findings.append({
|
|
94
|
+
"severity": "WARN",
|
|
95
|
+
"area": "evolution",
|
|
96
|
+
"msg": f"disabled: {obj.get('disabled_reason', 'unknown')}",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
summary = {
|
|
100
|
+
"timestamp": datetime.now().isoformat(),
|
|
101
|
+
"ok": not any(f["severity"] == "ERROR" for f in findings),
|
|
102
|
+
"integrity": integrity,
|
|
103
|
+
"cortex_running": cortex_running,
|
|
104
|
+
"evolution_enabled": evolution_enabled,
|
|
105
|
+
"restore_count_current_hour": restore_count,
|
|
106
|
+
"findings": findings,
|
|
107
|
+
}
|
|
108
|
+
SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
109
|
+
print(json.dumps(summary, ensure_ascii=False))
|
|
110
|
+
return 1 if any(f["severity"] == "ERROR" for f in findings) else 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
raise SystemExit(main())
|
package/src/server.py
CHANGED
|
@@ -110,7 +110,7 @@ def nexo_context_packet(area: str, files: str = "") -> str:
|
|
|
110
110
|
MUST call before delegating ANY task to a subagent. Inject the result into the subagent's prompt.
|
|
111
111
|
|
|
112
112
|
Args:
|
|
113
|
-
area: Project/area name (e.g., '
|
|
113
|
+
area: Project/area name (e.g., 'my-project', 'shopify', 'meta-ads', 'project_a', 'nexo', 'infrastructure').
|
|
114
114
|
files: Optional comma-separated file paths for additional context.
|
|
115
115
|
"""
|
|
116
116
|
return handle_context_packet(area, files)
|
|
@@ -308,7 +308,7 @@ def nexo_reminder_create(id: str, description: str, date: str = "", category: st
|
|
|
308
308
|
id: Unique ID starting with 'R' (e.g., R90).
|
|
309
309
|
description: What needs to be done.
|
|
310
310
|
date: Target date YYYY-MM-DD (optional).
|
|
311
|
-
category: One of:
|
|
311
|
+
category: One of: decisions, tasks, waiting, ideas, general.
|
|
312
312
|
"""
|
|
313
313
|
return handle_reminder_create(id, description, date, category)
|
|
314
314
|
|
|
@@ -329,7 +329,7 @@ def nexo_reminder_update(id: str, description: str = "", date: str = "", status:
|
|
|
329
329
|
|
|
330
330
|
@mcp.tool
|
|
331
331
|
def nexo_reminder_complete(id: str) -> str:
|
|
332
|
-
"""Mark a reminder as
|
|
332
|
+
"""Mark a reminder as COMPLETED with today's date.
|
|
333
333
|
|
|
334
334
|
Args:
|
|
335
335
|
id: Reminder ID (e.g., R87).
|
|
@@ -381,7 +381,7 @@ def nexo_followup_update(id: str, description: str = "", date: str = "", verific
|
|
|
381
381
|
|
|
382
382
|
@mcp.tool
|
|
383
383
|
def nexo_followup_complete(id: str, result: str = "") -> str:
|
|
384
|
-
"""Mark a followup as
|
|
384
|
+
"""Mark a followup as COMPLETED. Appends result to verification field.
|
|
385
385
|
|
|
386
386
|
Args:
|
|
387
387
|
id: Followup ID (e.g., NF45).
|
|
@@ -407,7 +407,7 @@ def nexo_learning_add(category: str, title: str, content: str, reasoning: str =
|
|
|
407
407
|
"""Add a new learning (resolved error, pattern, gotcha).
|
|
408
408
|
|
|
409
409
|
Args:
|
|
410
|
-
category: One of: nexo-ops, google-ads, meta-ads,
|
|
410
|
+
category: One of: nexo-ops, google-ads, meta-ads, analytics, shopify, ecommerce, cloud-sql, infrastructure, security, brain-engine.
|
|
411
411
|
title: Short title for the learning.
|
|
412
412
|
content: Full description with context and solution.
|
|
413
413
|
reasoning: WHY this matters — what led to discovering this (optional).
|
|
@@ -25,7 +25,7 @@ def handle_track(sid: str, paths: list[str]) -> str:
|
|
|
25
25
|
for f in c["files"]:
|
|
26
26
|
lines.append(f" {f}")
|
|
27
27
|
lines.append("")
|
|
28
|
-
lines.append("
|
|
28
|
+
lines.append("STOP and inform the user before editing.")
|
|
29
29
|
|
|
30
30
|
return "\n".join(lines)
|
|
31
31
|
|
|
@@ -66,7 +66,7 @@ def handle_files() -> str:
|
|
|
66
66
|
def handle_send(from_sid: str, to_sid: str, text: str) -> str:
|
|
67
67
|
"""Send a message. to_sid='all' for broadcast."""
|
|
68
68
|
msg_id = send_message(from_sid, to_sid, text)
|
|
69
|
-
target = "
|
|
69
|
+
target = "all sessions" if to_sid == "all" else to_sid
|
|
70
70
|
return f"Mensaje {msg_id} enviado a {target}."
|
|
71
71
|
|
|
72
72
|
|
|
@@ -99,4 +99,4 @@ def handle_check_answer(qid: str) -> str:
|
|
|
99
99
|
return f"RESPUESTA de {qid}: {result['answer']}"
|
|
100
100
|
elif result["status"] == "expired":
|
|
101
101
|
return f"Pregunta {qid} expirada sin respuesta."
|
|
102
|
-
return f"
|
|
102
|
+
return f"Question {qid} still pending. Retry in a few seconds."
|
package/src/tools_learnings.py
CHANGED
|
@@ -5,7 +5,7 @@ from db import (create_learning, update_learning, delete_learning, search_learni
|
|
|
5
5
|
|
|
6
6
|
VALID_CATEGORIES = {
|
|
7
7
|
"nexo-ops", "google-ads", "meta-ads", "google-analytics",
|
|
8
|
-
"shopify", "
|
|
8
|
+
"shopify", "my-project", "cloud-sql", "infrastructure", "security", "brain-engine"
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
|
package/src/tools_menu.py
CHANGED
|
@@ -11,52 +11,34 @@ from db import get_db
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _get_date_str() -> str:
|
|
14
|
-
"""Get formatted date
|
|
14
|
+
"""Get formatted current date and time."""
|
|
15
15
|
try:
|
|
16
16
|
result = subprocess.run(
|
|
17
|
-
["date", "+%A %d
|
|
17
|
+
["date", "+%A %d %B %Y, %H:%M"],
|
|
18
18
|
capture_output=True, text=True,
|
|
19
|
-
env={"
|
|
19
|
+
env={"PATH": "/usr/bin:/bin"}
|
|
20
20
|
)
|
|
21
21
|
return result.stdout.strip()
|
|
22
22
|
except Exception:
|
|
23
23
|
return datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
# Default menu items — customize for your operations.
|
|
27
|
+
# Each tuple: (category_name, [(item_number, item_description), ...])
|
|
26
28
|
MENU_ITEMS = [
|
|
27
|
-
("
|
|
28
|
-
("1", "
|
|
29
|
-
("
|
|
29
|
+
("System", [
|
|
30
|
+
("1", "Server health check"),
|
|
31
|
+
("2", "Review logs and errors"),
|
|
32
|
+
("3", "Check scheduled tasks status"),
|
|
30
33
|
]),
|
|
31
|
-
("
|
|
32
|
-
("
|
|
33
|
-
("
|
|
34
|
-
("
|
|
34
|
+
("Memory & Learning", [
|
|
35
|
+
("4", "Review pending learnings"),
|
|
36
|
+
("5", "Search learnings by topic"),
|
|
37
|
+
("6", "Memory health check"),
|
|
35
38
|
]),
|
|
36
|
-
("
|
|
37
|
-
("
|
|
38
|
-
("
|
|
39
|
-
("6", "Cambiar Promocion Shopify"),
|
|
40
|
-
]),
|
|
41
|
-
("Servidor e Infraestructura", [
|
|
42
|
-
("2", "Servidor - Chequeo server health check"),
|
|
43
|
-
("3", "WhatsApp Logs - Review WhatsApp logs"),
|
|
44
|
-
("11", "File Tracker - Reporte archivos PHP"),
|
|
45
|
-
("12", "Google Cloud - Gasto, consumo y estado GCP"),
|
|
46
|
-
]),
|
|
47
|
-
("Comunicacion y Monitorizacion", [
|
|
48
|
-
("8", "Recovery Optimizer - Analisis IA semanal (LUNES)"),
|
|
49
|
-
("10", "Recovery Monitor - Estado emails/WA recovery (24h)"),
|
|
50
|
-
("13", "Review Monitor - Estado emails/WA resenas"),
|
|
51
|
-
("14", "WhatsApp Analisis Completo - Estadisticas globales"),
|
|
52
|
-
("15", "Google Analytics - Revisar analiticas web"),
|
|
53
|
-
("16", "Email Review - Revisar bandejas y spam"),
|
|
54
|
-
]),
|
|
55
|
-
("Informes y SEO", [
|
|
56
|
-
("17", "Auditoria Search Console (cada 2 semanas)"),
|
|
57
|
-
("18", "Re-envio sitemaps (cada 30 dias)"),
|
|
58
|
-
("19", "Verificacion SEO metas"),
|
|
59
|
-
("20", "Informe Email Semanal (domingos)"),
|
|
39
|
+
("Analytics & Reports", [
|
|
40
|
+
("7", "Daily metrics overview"),
|
|
41
|
+
("8", "Weekly performance report"),
|
|
60
42
|
]),
|
|
61
43
|
]
|
|
62
44
|
|
|
@@ -108,7 +90,7 @@ def handle_menu() -> str:
|
|
|
108
90
|
|
|
109
91
|
lines = []
|
|
110
92
|
lines.append("╔" + "═" * W + "╗")
|
|
111
|
-
lines.append("║" + "NEXO —
|
|
93
|
+
lines.append("║" + "NEXO — OPERATIONS CENTER".center(W) + "║")
|
|
112
94
|
lines.append("║" + date_str.center(W) + "║")
|
|
113
95
|
lines.append("╠" + "═" * W + "╣")
|
|
114
96
|
|
|
@@ -130,12 +112,12 @@ def handle_menu() -> str:
|
|
|
130
112
|
lines.append("║" + f" {icon} {text}".ljust(W) + "║")
|
|
131
113
|
if len(dashboard_alerts) > 10:
|
|
132
114
|
more = len(dashboard_alerts) - 10
|
|
133
|
-
lines.append("║" + f" ...
|
|
115
|
+
lines.append("║" + f" ... and {more} more alerts".ljust(W) + "║")
|
|
134
116
|
|
|
135
117
|
if memory_reviews["total"] > 0:
|
|
136
118
|
text = (
|
|
137
|
-
f"MEMORIA: {memory_reviews['total']}
|
|
138
|
-
f"({memory_reviews['decisions']}
|
|
119
|
+
f"MEMORIA: {memory_reviews['total']} reviews pending "
|
|
120
|
+
f"({memory_reviews['decisions']} decisions, {memory_reviews['learnings']} learnings)"
|
|
139
121
|
)[:W - 4]
|
|
140
122
|
lines.append("║" + f" ! {text}".ljust(W) + "║")
|
|
141
123
|
|
|
@@ -156,26 +138,26 @@ def handle_menu() -> str:
|
|
|
156
138
|
lines.append("║" + entry.ljust(W) + "║")
|
|
157
139
|
lines.append("╠" + "═" * W + "╣")
|
|
158
140
|
|
|
159
|
-
# Backlog: ideas,
|
|
141
|
+
# Backlog: ideas, future projects, tasks without date or far ahead
|
|
160
142
|
try:
|
|
161
143
|
conn = get_db()
|
|
162
144
|
cutoff = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
|
|
163
|
-
# Reminders
|
|
145
|
+
# Reminders without date (backlog/ideas)
|
|
164
146
|
no_date = conn.execute(
|
|
165
|
-
"SELECT id, description, category FROM reminders WHERE status LIKE '
|
|
147
|
+
"SELECT id, description, category FROM reminders WHERE status LIKE 'PENDING%' AND (date IS NULL OR date='') ORDER BY category, id"
|
|
166
148
|
).fetchall()
|
|
167
|
-
# Reminders
|
|
149
|
+
# Reminders with date > 7 days (future)
|
|
168
150
|
future = conn.execute(
|
|
169
|
-
"SELECT id, description, date, category FROM reminders WHERE status LIKE '
|
|
151
|
+
"SELECT id, description, date, category FROM reminders WHERE status LIKE 'PENDING%' AND date > ? ORDER BY date",
|
|
170
152
|
(cutoff,)
|
|
171
153
|
).fetchall()
|
|
172
|
-
# Followups
|
|
154
|
+
# Followups without date
|
|
173
155
|
nf_no_date = conn.execute(
|
|
174
|
-
"SELECT id, description FROM followups WHERE status NOT LIKE '
|
|
156
|
+
"SELECT id, description FROM followups WHERE status NOT LIKE 'COMPLETED%' AND (date IS NULL OR date='') ORDER BY id"
|
|
175
157
|
).fetchall()
|
|
176
158
|
|
|
177
159
|
if no_date or future or nf_no_date:
|
|
178
|
-
lines.append("║" + " BACKLOG / IDEAS /
|
|
160
|
+
lines.append("║" + " BACKLOG / IDEAS / FUTURE".ljust(W) + "║")
|
|
179
161
|
lines.append("║" + "─" * W + "║")
|
|
180
162
|
|
|
181
163
|
if no_date:
|
|
@@ -190,20 +172,20 @@ def handle_menu() -> str:
|
|
|
190
172
|
lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
|
|
191
173
|
|
|
192
174
|
if future:
|
|
193
|
-
lines.append("║" + f" [
|
|
175
|
+
lines.append("║" + f" [Scheduled]".ljust(W) + "║")
|
|
194
176
|
for r in future:
|
|
195
177
|
short = r["description"][:W - 18]
|
|
196
178
|
lines.append("║" + f" {r['id']} ({r['date']}): {short}".ljust(W) + "║")
|
|
197
179
|
|
|
198
180
|
if nf_no_date:
|
|
199
|
-
lines.append("║" + f" [
|
|
181
|
+
lines.append("║" + f" [Pending followups]".ljust(W) + "║")
|
|
200
182
|
for r in nf_no_date:
|
|
201
183
|
short = r["description"][:W - 12]
|
|
202
184
|
lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
|
|
203
185
|
|
|
204
186
|
lines.append("╠" + "═" * W + "╣")
|
|
205
187
|
except Exception as e:
|
|
206
|
-
lines.append("║" + f" ⚠
|
|
188
|
+
lines.append("║" + f" ⚠ Backlog error: {e}".ljust(W) + "║")
|
|
207
189
|
lines.append("╠" + "═" * W + "╣")
|
|
208
190
|
|
|
209
191
|
# Active sessions
|
|
@@ -139,7 +139,7 @@ def handle_followup_complete(id: str, result: str = '') -> str:
|
|
|
139
139
|
msg += f" ♻️ Siguiente auto-creado para {new_row['date']}."
|
|
140
140
|
linked_decisions = find_decisions_by_context_ref(id)
|
|
141
141
|
if linked_decisions:
|
|
142
|
-
outcome_text = result if result else f"Followup {id}
|
|
142
|
+
outcome_text = result if result else f"Followup {id} completed"
|
|
143
143
|
for dec in linked_decisions:
|
|
144
144
|
update_decision_outcome(dec['id'], outcome_text)
|
|
145
145
|
dec_ids = ', '.join(f"#{d['id']}" for d in linked_decisions)
|
package/src/tools_sessions.py
CHANGED
|
@@ -76,20 +76,20 @@ def handle_startup(task: str = "Startup") -> str:
|
|
|
76
76
|
lines = [f"SID: {sid}"]
|
|
77
77
|
|
|
78
78
|
if cleaned > 0:
|
|
79
|
-
lines.append(f"
|
|
79
|
+
lines.append(f"Cleaned {cleaned} stale sessions.")
|
|
80
80
|
|
|
81
81
|
if other_sessions:
|
|
82
82
|
lines.append("")
|
|
83
|
-
lines.append("
|
|
83
|
+
lines.append("ACTIVE SESSIONS:")
|
|
84
84
|
for s in other_sessions:
|
|
85
85
|
age = _format_age(s["last_update_epoch"])
|
|
86
86
|
lines.append(f" {s['sid']} ({age}) — {s['task']}")
|
|
87
87
|
else:
|
|
88
|
-
lines.append("
|
|
88
|
+
lines.append("No other active sessions.")
|
|
89
89
|
|
|
90
90
|
if inbox:
|
|
91
91
|
lines.append("")
|
|
92
|
-
lines.append("
|
|
92
|
+
lines.append("PENDING MESSAGES:")
|
|
93
93
|
for m in inbox:
|
|
94
94
|
age = _format_age(m["created_epoch"])
|
|
95
95
|
lines.append(f" [{m['from_sid']}] ({age}): {m['text']}")
|
|
@@ -121,7 +121,7 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
121
121
|
questions = get_pending_questions(sid)
|
|
122
122
|
if questions:
|
|
123
123
|
parts.append("")
|
|
124
|
-
parts.append("
|
|
124
|
+
parts.append("PENDING QUESTIONS (responder con nexo_answer):")
|
|
125
125
|
for q in questions:
|
|
126
126
|
age = _format_age(q["created_epoch"])
|
|
127
127
|
parts.append(f" {q['qid']} de {q['from_sid']} ({age}): {q['question']}")
|
|
@@ -236,7 +236,7 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
|
236
236
|
# Auto-prime: detect project/area keywords and fetch targeted learnings
|
|
237
237
|
_PROJECT_KEYWORDS = {
|
|
238
238
|
'shopify': 'shopify', 'theme': 'shopify', 'dawn': 'shopify',
|
|
239
|
-
'
|
|
239
|
+
'my-project': 'my-project',
|
|
240
240
|
'meta': 'meta-ads', 'facebook': 'meta-ads', 'advantage': 'meta-ads',
|
|
241
241
|
'google ads': 'google-ads', 'pmax': 'google-ads', 'campaign': 'google-ads',
|
|
242
242
|
'server': 'infrastructure', 'ssh': 'infrastructure', 'deploy': 'infrastructure',
|
|
@@ -380,7 +380,7 @@ def handle_context_packet(area: str, files: str = "") -> str:
|
|
|
380
380
|
|
|
381
381
|
# 3. Active followups for this area
|
|
382
382
|
followups = conn.execute(
|
|
383
|
-
"SELECT id, description, date, verification FROM followups WHERE status
|
|
383
|
+
"SELECT id, description, date, verification FROM followups WHERE status NOT LIKE 'COMPLET%' AND (description LIKE ? OR verification LIKE ?) ORDER BY date ASC LIMIT 10",
|
|
384
384
|
(f"%{area}%", f"%{area}%")
|
|
385
385
|
).fetchall()
|
|
386
386
|
if followups:
|
|
@@ -451,14 +451,14 @@ def handle_smart_startup_query() -> str:
|
|
|
451
451
|
|
|
452
452
|
# 1. Pending followups (what NEXO needs to do)
|
|
453
453
|
followups = conn.execute(
|
|
454
|
-
"SELECT description FROM followups WHERE status
|
|
454
|
+
"SELECT description FROM followups WHERE status NOT LIKE 'COMPLET%' ORDER BY date ASC LIMIT 5"
|
|
455
455
|
).fetchall()
|
|
456
456
|
for f in followups:
|
|
457
457
|
query_parts.append(f['description'][:100])
|
|
458
458
|
|
|
459
459
|
# 2. Due reminders (what the owner needs to know)
|
|
460
460
|
reminders = conn.execute(
|
|
461
|
-
"SELECT description FROM reminders WHERE status
|
|
461
|
+
"SELECT description FROM reminders WHERE status NOT LIKE 'COMPLET%' AND date <= date('now', '+1 day') ORDER BY date ASC LIMIT 5"
|
|
462
462
|
).fetchall()
|
|
463
463
|
for r in reminders:
|
|
464
464
|
query_parts.append(r['description'][:100])
|
|
@@ -466,7 +466,7 @@ def handle_smart_startup_query() -> str:
|
|
|
466
466
|
# 3. Last session diary topics
|
|
467
467
|
try:
|
|
468
468
|
last_diary = conn.execute(
|
|
469
|
-
"SELECT summary FROM
|
|
469
|
+
"SELECT summary FROM session_diary ORDER BY id DESC LIMIT 1"
|
|
470
470
|
).fetchone()
|
|
471
471
|
if last_diary and last_diary['summary']:
|
|
472
472
|
query_parts.append(last_diary['summary'][:200])
|
|
@@ -517,9 +517,9 @@ def handle_status(keyword: str | None = None) -> str:
|
|
|
517
517
|
sessions = get_active_sessions()
|
|
518
518
|
|
|
519
519
|
if not sessions:
|
|
520
|
-
return "
|
|
520
|
+
return "No active sessions."
|
|
521
521
|
|
|
522
|
-
lines = ["
|
|
522
|
+
lines = ["ACTIVE SESSIONS:"]
|
|
523
523
|
for s in sessions:
|
|
524
524
|
age = _format_age(s["last_update_epoch"])
|
|
525
525
|
lines.append(f" {s['sid']} ({age}) — {s['task']}")
|
package/src/rules/__init__ 2.py
DELETED
|
File without changes
|