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.
Files changed (90) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  4. package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
  5. package/src/__pycache__/db.cpython-314.pyc +0 -0
  6. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  7. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  8. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  9. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  10. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  11. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  12. package/src/__pycache__/server.cpython-314.pyc +0 -0
  13. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  14. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  15. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  16. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  17. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  19. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  20. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  21. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  22. package/src/cognitive.py +63 -14
  23. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  24. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  25. package/src/dashboard/app.py +1 -1
  26. package/src/db.py +29 -13
  27. package/src/evolution_cycle.py +72 -94
  28. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  29. package/src/hooks/session-start.sh +5 -2
  30. package/src/hooks/session-stop.sh +79 -133
  31. package/src/knowledge_graph.py +3 -3
  32. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  34. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  35. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  36. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  37. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  38. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  39. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  40. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  41. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  42. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  43. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  44. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  45. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  46. package/src/plugins/artifact_registry.py +450 -0
  47. package/src/plugins/cognitive_memory.py +9 -9
  48. package/src/plugins/episodic_memory.py +8 -8
  49. package/src/plugins/evolution.py +4 -4
  50. package/src/plugins/guard.py +26 -2
  51. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  53. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  54. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  55. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  56. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  57. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  58. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  59. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  60. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  61. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  62. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  63. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  64. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  65. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  66. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  67. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  68. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  69. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  70. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  71. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  72. package/src/scripts/nexo-brain-activation.sh +140 -0
  73. package/src/scripts/nexo-evolution-run.py +1 -1
  74. package/src/scripts/nexo-followup-hygiene.py +107 -0
  75. package/src/scripts/nexo-postmortem-consolidator.py +25 -25
  76. package/src/scripts/nexo-pre-commit.py +118 -0
  77. package/src/scripts/nexo-proactive-dashboard.py +342 -0
  78. package/src/scripts/nexo-runtime-preflight.py +270 -0
  79. package/src/scripts/nexo-send-email.py +25 -0
  80. package/src/scripts/nexo-send-reply.py +176 -0
  81. package/src/scripts/nexo-snapshot-restore.sh +34 -0
  82. package/src/scripts/nexo-watchdog-smoke.py +114 -0
  83. package/src/server.py +5 -5
  84. package/src/tools_coordination.py +3 -3
  85. package/src/tools_learnings.py +1 -1
  86. package/src/tools_menu.py +31 -49
  87. package/src/tools_reminders_crud.py +1 -1
  88. package/src/tools_sessions.py +12 -12
  89. package/src/rules/__init__ 2.py +0 -0
  90. 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., 'wazion', 'shopify', 'meta-ads', 'project_a', 'nexo', 'infrastructure').
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: decisiones, tareas, esperando, ideas, general.
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 COMPLETADO with today's date.
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 COMPLETADO. Appends result to verification field.
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, google-analytics, shopify, wazion, cloud-sql, infrastructure, security, brain-engine.
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("PARAR e informar a Francisco antes de editar.")
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 = "todas las sesiones" if to_sid == "all" else to_sid
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"Pregunta {qid} sigue pendiente. Reintentar en unos segundos."
102
+ return f"Question {qid} still pending. Retry in a few seconds."
@@ -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", "wazion", "cloud-sql", "infrastructure", "security", "brain-engine"
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 in Madrid timezone."""
14
+ """Get formatted current date and time."""
15
15
  try:
16
16
  result = subprocess.run(
17
- ["date", "+%A %d de %B de %Y, %H:%M"],
17
+ ["date", "+%A %d %B %Y, %H:%M"],
18
18
  capture_output=True, text=True,
19
- env={"TZ": "Europe/Madrid", "PATH": "/usr/bin:/bin", "LANG": "es_ES.UTF-8"}
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
- ("Proyectos", [
28
- ("1", "WAzion - Revisar estado del proyecto"),
29
- ("9", "Claude Agent VPS - Revisar cambios autonomos"),
29
+ ("System", [
30
+ ("1", "Server health check"),
31
+ ("2", "Review logs and errors"),
32
+ ("3", "Check scheduled tasks status"),
30
33
  ]),
31
- ("Publicidad", [
32
- ("7", "Google Ads - Administrar campanas (Recambios + WAzion)"),
33
- ("7b", "Meta Ads - Administrar campanas Facebook/Instagram"),
34
- ("7c", "Ads Tracking - Revision combinada Google+Meta"),
34
+ ("Memory & Learning", [
35
+ ("4", "Review pending learnings"),
36
+ ("5", "Search learnings by topic"),
37
+ ("6", "Memory health check"),
35
38
  ]),
36
- ("Shopify", [
37
- ("4", "Shopify Theme Sync - Sincronizar tema"),
38
- ("5", "Shopify Scripts - Ejecutar scripts periodicos"),
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 — CENTRO DE OPERACIONES".center(W) + "║")
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" ... y {more} alertas mas".ljust(W) + "║")
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']} revisiones pendientes "
138
- f"({memory_reviews['decisions']} decisiones, {memory_reviews['learnings']} learnings)"
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, proyectos futuros, tareas sin fecha o lejanas
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 sin fecha (backlog/ideas)
145
+ # Reminders without date (backlog/ideas)
164
146
  no_date = conn.execute(
165
- "SELECT id, description, category FROM reminders WHERE status LIKE 'PENDIENTE%' AND (date IS NULL OR date='') ORDER BY category, id"
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 con fecha > 7 días (futuro)
149
+ # Reminders with date > 7 days (future)
168
150
  future = conn.execute(
169
- "SELECT id, description, date, category FROM reminders WHERE status LIKE 'PENDIENTE%' AND date > ? ORDER BY date",
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 sin fecha
154
+ # Followups without date
173
155
  nf_no_date = conn.execute(
174
- "SELECT id, description FROM followups WHERE status NOT LIKE 'COMPLETADO%' AND (date IS NULL OR date='') ORDER BY id"
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 / FUTURO".ljust(W) + "║")
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" [Programado]".ljust(W) + "║")
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" [Followups pendientes]".ljust(W) + "║")
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" ⚠ Error backlog: {e}".ljust(W) + "║")
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} completado"
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)
@@ -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"Limpiadas {cleaned} sesiones stale.")
79
+ lines.append(f"Cleaned {cleaned} stale sessions.")
80
80
 
81
81
  if other_sessions:
82
82
  lines.append("")
83
- lines.append("SESIONES ACTIVAS:")
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("Sin otras sesiones activas.")
88
+ lines.append("No other active sessions.")
89
89
 
90
90
  if inbox:
91
91
  lines.append("")
92
- lines.append("MENSAJES PENDIENTES:")
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("PREGUNTAS PENDIENTES (responder con nexo_answer):")
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
- 'wazion': 'wazion', 'chrome extension': 'wazion', 'vps': 'wazion',
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 = 'PENDIENTE' AND (description LIKE ? OR verification LIKE ?) ORDER BY date ASC LIMIT 10",
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 = 'PENDIENTE' ORDER BY date ASC LIMIT 5"
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 = 'PENDIENTE' AND date <= date('now', '+1 day') ORDER BY date ASC LIMIT 5"
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 session_diaries ORDER BY id DESC LIMIT 1"
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 "Sin sesiones activas."
520
+ return "No active sessions."
521
521
 
522
- lines = ["SESIONES ACTIVAS:"]
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']}")
File without changes