nexo-brain 0.8.8 → 0.8.10

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 CHANGED
@@ -10,7 +10,13 @@
10
10
 
11
11
  **NEXO Brain transforms any MCP-compatible AI agent from a stateless assistant into a cognitive partner that remembers, learns, forgets, adapts, and builds a relationship with you over time.**
12
12
 
13
- [Watch the overview on YouTube](https://www.youtube.com/watch?v=-uvhicUhGTY)
13
+ <p align="center">
14
+ <a href="https://www.youtube.com/watch?v=J0hCWnYU4UY">
15
+ <img src="assets/nexo-brain-infographic-v4.png" alt="NEXO Brain Architecture" width="700">
16
+ </a>
17
+ </p>
18
+
19
+ [Watch the 1-minute overview on YouTube](https://www.youtube.com/watch?v=J0hCWnYU4UY)
14
20
 
15
21
  Every time you close a session, everything is lost. Your agent doesn't remember yesterday's decisions, repeats the same mistakes, and starts from zero. NEXO Brain fixes this with a cognitive architecture modeled after how human memory actually works.
16
22
 
@@ -268,7 +274,9 @@ A bi-temporal entity-relationship graph with 988 nodes and 896 edges. Entities a
268
274
  A visual interface at `localhost:6174` with 6 pages: Overview (system health at a glance), Graph (interactive D3.js visualization of the knowledge graph), Memory (browse and search all memory stores), Somatic (pain map per file/area), Adaptive (personality signals and weights), and Sessions (active and historical sessions). Built with FastAPI backend and D3.js frontend.
269
275
 
270
276
  ### Cross-Platform Support
271
- v0.8.0 adds full Linux and Windows support. The installer detects the platform and configures the appropriate process manager (LaunchAgents on macOS, systemd on Linux, Task Scheduler on Windows). Opportunistic maintenance runs cognitive processes when resources are available.
277
+ v0.8.0 adds full Linux support and Windows via WSL. The installer detects the platform and configures the appropriate process manager (LaunchAgents on macOS, catch-up on startup for Linux). Opportunistic maintenance runs cognitive processes when resources are available.
278
+
279
+ > **Windows users:** NEXO Brain requires [WSL (Windows Subsystem for Linux)](https://learn.microsoft.com/en-us/windows/wsl/install). Install WSL first, then run `npx nexo-brain` inside the Ubuntu/WSL terminal.
272
280
 
273
281
  ### Storage Router
274
282
  A new abstraction layer routes storage operations through a unified interface, making the system multi-tenant ready. Each operator's data is isolated while sharing the same cognitive engine.
@@ -343,7 +351,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
343
351
 
344
352
  ### Requirements
345
353
 
346
- - **macOS, Linux, or Windows**
354
+ - **macOS or Linux** (Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install))
347
355
  - **Node.js 18+** (for the installer)
348
356
  - **Claude Opus (latest version) strongly recommended.** NEXO Brain provides 109+ MCP tools across 19 categories. This cognitive load requires a top-tier model with large context window. Smaller models (Haiku, Sonnet) may struggle with tool selection and produce inconsistent results. Opus handles all 109+ tools without hesitation.
349
357
  - Python 3, Homebrew, and Claude Code are installed automatically if missing.
package/bin/nexo-brain.js CHANGED
@@ -77,8 +77,14 @@ async function main() {
77
77
 
78
78
  // Check prerequisites
79
79
  const platform = process.platform;
80
- if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
81
- log(`Unsupported platform: ${platform}. NEXO supports macOS, Linux, and Windows.`);
80
+ if (platform === "win32") {
81
+ log("Windows detected. NEXO Brain requires WSL (Windows Subsystem for Linux).");
82
+ log("Install WSL: https://learn.microsoft.com/en-us/windows/wsl/install");
83
+ log("Then run this command inside WSL (Ubuntu terminal), not PowerShell/CMD.");
84
+ process.exit(1);
85
+ }
86
+ if (platform !== "darwin" && platform !== "linux") {
87
+ log(`Unsupported platform: ${platform}. NEXO supports macOS and Linux (Windows via WSL).`);
82
88
  process.exit(1);
83
89
  }
84
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
6
6
  "bin": {
package/src/cognitive.py CHANGED
@@ -30,13 +30,13 @@ DISCRIMINATING_ENTITIES = {
30
30
  # OS / Environment
31
31
  "linux", "mac", "macos", "windows", "darwin", "ubuntu", "debian", "alpine",
32
32
  # Platforms
33
- "shopify", "whatsapp", "chrome", "firefox",
33
+ "shopify", "wazion", "canarirural", "recambios", "whatsapp", "chrome", "firefox",
34
34
  # Languages / Runtimes
35
35
  "python", "php", "javascript", "typescript", "node", "deno", "ruby",
36
36
  # Versions
37
37
  "v1", "v2", "v3", "v4", "v5", "5.6", "7.4", "8.0", "8.1", "8.2",
38
38
  # Infrastructure
39
- "cloudrun", "gcloud", "vps", "local", "production", "staging",
39
+ "mundiserver", "cloudrun", "gcloud", "vps", "local", "production", "staging",
40
40
  # DB
41
41
  "mysql", "sqlite", "postgresql", "postgres", "redis",
42
42
  }
@@ -65,12 +65,12 @@ URGENCY_SIGNALS = {
65
65
  _DEFAULT_TRUST_EVENTS = {
66
66
  # Positive
67
67
  "explicit_thanks": +3,
68
- "delegation": +2, # User delegates new task without micromanaging
69
- "paradigm_shift": +2, # User teaches, NEXO learns
68
+ "delegation": +2, # Francisco delegates new task without micromanaging
69
+ "paradigm_shift": +2, # Francisco teaches, NEXO learns
70
70
  "sibling_detected": +3, # NEXO avoided context error on its own
71
71
  "proactive_action": +2, # NEXO did something useful without being asked
72
72
  # Negative
73
- "correction": -3, # User corrects NEXO
73
+ "correction": -3, # Francisco corrects NEXO
74
74
  "repeated_error": -7, # Error on something NEXO already had a learning for
75
75
  "override": -5, # NEXO's memory was wrong
76
76
  "correction_fatigue": -10, # Same memory corrected 3+ times
@@ -395,7 +395,7 @@ def _init_tables(conn: sqlite3.Connection):
395
395
  created_at TEXT DEFAULT (datetime('now'))
396
396
  );
397
397
 
398
- -- Sentiment readings: User's detected mood per interaction
398
+ -- Sentiment readings: Francisco's detected mood per interaction
399
399
  CREATE TABLE IF NOT EXISTS sentiment_log (
400
400
  id INTEGER PRIMARY KEY AUTOINCREMENT,
401
401
  sentiment TEXT NOT NULL, -- 'positive', 'negative', 'neutral', 'urgent'
@@ -421,13 +421,13 @@ def _init_tables(conn: sqlite3.Connection):
421
421
  status TEXT DEFAULT 'pending'
422
422
  );
423
423
 
424
- -- Correction tracking: when User overrides a memory's guidance
424
+ -- Correction tracking: when Francisco overrides a memory's guidance
425
425
  CREATE TABLE IF NOT EXISTS memory_corrections (
426
426
  id INTEGER PRIMARY KEY AUTOINCREMENT,
427
427
  memory_id INTEGER NOT NULL,
428
428
  store TEXT NOT NULL, -- 'stm' or 'ltm'
429
429
  correction_type TEXT NOT NULL, -- 'override', 'exception', 'paradigm_shift'
430
- context TEXT DEFAULT '', -- what User said
430
+ context TEXT DEFAULT '', -- what Francisco said
431
431
  created_at TEXT DEFAULT (datetime('now'))
432
432
  );
433
433
  """)
@@ -2544,12 +2544,12 @@ def get_siblings(memory_id: int) -> list[dict]:
2544
2544
  def detect_dissonance(new_instruction: str, min_score: float = 0.65) -> list[dict]:
2545
2545
  """Detect cognitive dissonance: find LTM memories that contradict a new instruction.
2546
2546
 
2547
- When User gives a new instruction that conflicts with established LTM memories
2547
+ When Francisco gives a new instruction that conflicts with established LTM memories
2548
2548
  (strength > 0.8), this function surfaces the conflict so NEXO can verbalize it
2549
2549
  rather than silently obeying or silently resisting.
2550
2550
 
2551
2551
  Args:
2552
- new_instruction: The new instruction or preference from User
2552
+ new_instruction: The new instruction or preference from Francisco
2553
2553
  min_score: Minimum cosine similarity to consider as potential conflict
2554
2554
 
2555
2555
  Returns:
@@ -2584,12 +2584,12 @@ def detect_dissonance(new_instruction: str, min_score: float = 0.65) -> list[dic
2584
2584
 
2585
2585
 
2586
2586
  def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> str:
2587
- """Resolve a cognitive dissonance by applying User's decision.
2587
+ """Resolve a cognitive dissonance by applying Francisco's decision.
2588
2588
 
2589
2589
  Args:
2590
2590
  memory_id: The LTM memory that conflicts with the new instruction
2591
2591
  resolution: One of:
2592
- - 'paradigm_shift': User changed his mind permanently. Decay old memory,
2592
+ - 'paradigm_shift': Francisco changed his mind permanently. Decay old memory,
2593
2593
  new instruction becomes the standard.
2594
2594
  - 'exception': This is a one-time override. Keep old memory as standard.
2595
2595
  - 'override': Old memory was wrong. Mark as corrupted and decay to dormant.
@@ -2640,7 +2640,7 @@ def resolve_dissonance(memory_id: int, resolution: str, context: str = "") -> st
2640
2640
  def check_correction_fatigue() -> list[dict]:
2641
2641
  """Find memories corrected 3+ times in the last 7 days — mark as 'under review'.
2642
2642
 
2643
- These memories are unreliable: User keeps overriding them, suggesting
2643
+ These memories are unreliable: Francisco keeps overriding them, suggesting
2644
2644
  the memory itself may be wrong or outdated.
2645
2645
 
2646
2646
  Returns:
@@ -2688,7 +2688,7 @@ def check_correction_fatigue() -> list[dict]:
2688
2688
 
2689
2689
 
2690
2690
  def detect_sentiment(text: str) -> dict:
2691
- """Analyze User's text for sentiment signals.
2691
+ """Analyze Francisco's text for sentiment signals.
2692
2692
 
2693
2693
  Returns detected sentiment, intensity, and action guidance for NEXO.
2694
2694
  Not a model — keyword + heuristic based. Fast and deterministic.
@@ -2727,13 +2727,13 @@ def detect_sentiment(text: str) -> dict:
2727
2727
  sentiment = "negative"
2728
2728
  intensity = min(1.0, 0.3 + neg_score * 0.15)
2729
2729
  if intensity > 0.7:
2730
- guidance = "MODE: Ultra-conciso. Cero explicaciones. Resolver y mostrar resultado."
2730
+ guidance = "MODE: Ultra-concise. Zero explanations. Resolve and show result."
2731
2731
  else:
2732
- guidance = "MODE: Conciso. Menos contexto, más acción directa."
2732
+ guidance = "MODE: Concise. Less context, more direct action."
2733
2733
  elif pos_score > neg_score and pos_score >= 1:
2734
2734
  sentiment = "positive"
2735
2735
  intensity = min(1.0, 0.3 + pos_score * 0.15)
2736
- guidance = "MODE: Normal. Buen momento para proponer ideas de backlog o mejoras."
2736
+ guidance = "MODE: Normal. Good moment to propose backlog ideas or improvements."
2737
2737
  elif urgency_hits:
2738
2738
  sentiment = "urgent"
2739
2739
  intensity = 0.8
@@ -2752,7 +2752,7 @@ def detect_sentiment(text: str) -> dict:
2752
2752
 
2753
2753
 
2754
2754
  def log_sentiment(text: str) -> dict:
2755
- """Detect and log User's sentiment. Returns the detection result."""
2755
+ """Detect and log Francisco's sentiment. Returns the detection result."""
2756
2756
  result = detect_sentiment(text)
2757
2757
  if result["sentiment"] != "neutral":
2758
2758
  db = _get_db()
package/src/db.py CHANGED
@@ -437,7 +437,7 @@ _SYNONYMS = {
437
437
  "plantilla": ["template"],
438
438
  "template": ["plantilla"],
439
439
  "webhook": ["gancho"],
440
- "cron": ["scheduled task", "scheduled"],
440
+ "cron": ["tarea programada", "scheduled"],
441
441
  "extension": ["extensión", "plugin", "addon"],
442
442
  "plugin": ["extension", "extensión"],
443
443
  }
@@ -1012,24 +1012,31 @@ def register_session(sid: str, task: str) -> dict:
1012
1012
  return {"sid": sid, "task": task}
1013
1013
 
1014
1014
 
1015
- def update_session(sid: str, task: str) -> dict:
1016
- """Update session task and timestamp. Preserves started_epoch."""
1015
+ def update_session(sid: str, task: str | None) -> dict:
1016
+ """Update session timestamp (and task if provided). Preserves started_epoch.
1017
+
1018
+ Args:
1019
+ sid: Session ID.
1020
+ task: New task description, or None to keep current task (keepalive touch).
1021
+ """
1017
1022
  conn = get_db()
1018
1023
  now = now_epoch()
1019
- row = conn.execute("SELECT started_epoch FROM sessions WHERE sid = ?", (sid,)).fetchone()
1024
+ row = conn.execute("SELECT started_epoch, task FROM sessions WHERE sid = ?", (sid,)).fetchone()
1020
1025
  if row:
1026
+ effective_task = task if task is not None else row["task"]
1021
1027
  conn.execute(
1022
1028
  "UPDATE sessions SET task = ?, last_update_epoch = ?, local_time = ? WHERE sid = ?",
1023
- (task, now, local_time_str(), sid)
1029
+ (effective_task, now, local_time_str(), sid)
1024
1030
  )
1025
1031
  else:
1032
+ effective_task = task or "Unknown"
1026
1033
  conn.execute(
1027
1034
  "INSERT INTO sessions (sid, task, started_epoch, last_update_epoch, local_time) "
1028
1035
  "VALUES (?, ?, ?, ?, ?)",
1029
- (sid, task, now, now, local_time_str())
1036
+ (sid, effective_task, now, now, local_time_str())
1030
1037
  )
1031
1038
  conn.commit()
1032
- return {"sid": sid, "task": task}
1039
+ return {"sid": sid, "task": effective_task}
1033
1040
 
1034
1041
 
1035
1042
  def complete_session(sid: str):
@@ -5,10 +5,10 @@ def handle_agent_get(id: str) -> str:
5
5
  """Get an agent's full profile by ID."""
6
6
  a = get_agent(id)
7
7
  if not a: return f"Agent '{id}' not found."
8
- lines = [f"AGENTE: {a['name']} ({a['id']})", f" Especialización: {a['specialization']}", f" Modelo: {a['model']}"]
8
+ lines = [f"AGENT: {a['name']} ({a['id']})", f" Specialization: {a['specialization']}", f" Model: {a['model']}"]
9
9
  if a["tools"]: lines.append(f" Tools: {a['tools']}")
10
- if a["context_files"]: lines.append(f" Contexto: {a['context_files']}")
11
- if a["rules"]: lines.append(f" Reglas: {a['rules']}")
10
+ if a["context_files"]: lines.append(f" Context: {a['context_files']}")
11
+ if a["rules"]: lines.append(f" Rules: {a['rules']}")
12
12
  return "\n".join(lines)
13
13
 
14
14
  def handle_agent_create(id: str, name: str, specialization: str, model: str = "sonnet",
@@ -31,8 +31,8 @@ def handle_agent_update(id: str, name: str = "", specialization: str = "", model
31
31
  def handle_agent_list() -> str:
32
32
  """List all registered agents."""
33
33
  agents = list_agents()
34
- if not agents: return "No agents registered."
35
- lines = ["AGENTES REGISTRADOS:"]
34
+ if not agents: return "No registered agents."
35
+ lines = ["REGISTERED AGENTS:"]
36
36
  for a in agents:
37
37
  lines.append(f" {a['id']} — {a['name']} ({a['model']}) — {a['specialization'][:60]}")
38
38
  return "\n".join(lines)
@@ -33,10 +33,10 @@ def handle_backup_now() -> str:
33
33
  def handle_backup_list() -> str:
34
34
  """List available backups with dates and sizes."""
35
35
  if not os.path.isdir(BACKUP_DIR):
36
- return "No backups found."
36
+ return "No backups."
37
37
  files = sorted(glob.glob(os.path.join(BACKUP_DIR, "nexo-*.db")), reverse=True)
38
38
  if not files:
39
- return "No backups found."
39
+ return "No backups."
40
40
  lines = [f"BACKUPS ({len(files)}):"]
41
41
  total_size = 0
42
42
  for f in files:
@@ -222,13 +222,13 @@ def handle_cognitive_metrics(days: int = 7) -> str:
222
222
 
223
223
 
224
224
  def handle_cognitive_sentiment(text: str) -> str:
225
- """Detect the user's sentiment from his text. Returns mood, intensity, and guidance.
225
+ """Detect Francisco's sentiment from his text. Returns mood, intensity, and guidance.
226
226
 
227
- Call this with the user's recent message to adapt NEXO's tone and behavior.
227
+ Call this with Francisco's recent message to adapt NEXO's tone and behavior.
228
228
  Also logs the sentiment for historical tracking.
229
229
 
230
230
  Args:
231
- text: the user's recent message or instruction
231
+ text: Francisco's recent message or instruction
232
232
  """
233
233
  result = cognitive.log_sentiment(text)
234
234
  trust = cognitive.get_trust_score()
@@ -294,8 +294,8 @@ def handle_cognitive_trust(event: str = '', context: str = '', delta: float = No
294
294
  def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
295
295
  """Detect cognitive dissonance: find established memories that conflict with a new instruction.
296
296
 
297
- Use BEFORE applying a new preference or rule from the user that might contradict
298
- existing knowledge. If conflicts found, verbalize them and ask the user to resolve.
297
+ Use BEFORE applying a new preference or rule from Francisco that might contradict
298
+ existing knowledge. If conflicts found, verbalize them and ask Francisco to resolve.
299
299
 
300
300
  Args:
301
301
  instruction: The new instruction or preference to check against LTM
@@ -328,7 +328,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
328
328
  lines.append("")
329
329
 
330
330
  lines.append("RESOLVE with nexo_cognitive_resolve, or use force=True to skip:")
331
- lines.append(" - 'paradigm_shift': the user changed his mind permanently.")
331
+ lines.append(" - 'paradigm_shift': Francisco changed his mind permanently.")
332
332
  lines.append(" - 'exception': One-time override. Old memory stays.")
333
333
  lines.append(" - 'override': Old memory was wrong.")
334
334
 
@@ -336,7 +336,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
336
336
 
337
337
 
338
338
  def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '') -> str:
339
- """Resolve a cognitive dissonance by applying the user's decision.
339
+ """Resolve a cognitive dissonance by applying Francisco's decision.
340
340
 
341
341
  Args:
342
342
  memory_id: The LTM memory ID from the dissonance detection
@@ -546,7 +546,7 @@ TOOLS = [
546
546
  (handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation (spec section 9)"),
547
547
  (handle_cognitive_dissonance, "nexo_cognitive_dissonance", "Detect conflicts between a new instruction and established LTM memories. force=True to skip discussion."),
548
548
  (handle_cognitive_resolve, "nexo_cognitive_resolve", "Resolve a cognitive dissonance: paradigm_shift, exception, or override."),
549
- (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect the user's sentiment and get tone guidance. Also logs for tracking."),
549
+ (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect Francisco's sentiment and get tone guidance. Also logs for tracking."),
550
550
  (handle_cognitive_trust, "nexo_cognitive_trust", "View or adjust trust score (0-100). Without args: view. With event: adjust."),
551
551
  (handle_cognitive_pin, "nexo_cognitive_pin", "Pin a memory — never decays, boosted +0.2 in search results."),
552
552
  (handle_cognitive_snooze, "nexo_cognitive_snooze", "Snooze a memory — hidden from searches until a date, then auto-restores."),
@@ -21,7 +21,7 @@ def handle_entity_create(name: str, type: str, value: str, notes: str = "") -> s
21
21
  on_entity_create(eid, name, type)
22
22
  except Exception:
23
23
  pass
24
- return f"Entidad creada: [{eid}] {name} ({type})"
24
+ return f"Entity created: [{eid}] {name} ({type})"
25
25
 
26
26
  def handle_entity_update(id: int, name: str = "", type: str = "", value: str = "", notes: str = "") -> str:
27
27
  """Update an entity. Only non-empty fields are changed."""
@@ -30,21 +30,21 @@ def handle_entity_update(id: int, name: str = "", type: str = "", value: str = "
30
30
  if type: kwargs["type"] = type
31
31
  if value: kwargs["value"] = value
32
32
  if notes: kwargs["notes"] = notes
33
- if not kwargs: return "Nada que actualizar."
33
+ if not kwargs: return "Nothing to update."
34
34
  update_entity(id, **kwargs)
35
- return f"Entidad [{id}] actualizada."
35
+ return f"Entity [{id}] updated."
36
36
 
37
37
  def handle_entity_delete(id: int) -> str:
38
38
  """Delete an entity."""
39
39
  if not delete_entity(id):
40
40
  return f"ERROR: Entity [{id}] not found."
41
- return f"Entidad [{id}] eliminada."
41
+ return f"Entity [{id}] deleted."
42
42
 
43
43
  def handle_entity_list(type: str = "") -> str:
44
44
  """List all entities, optionally filtered by type."""
45
45
  results = list_entities(type)
46
46
  if not results:
47
- return "No entities found."
47
+ return "No entities."
48
48
  grouped = {}
49
49
  for e in results:
50
50
  t = e["type"]
@@ -27,7 +27,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
27
27
  """Log a non-trivial decision with reasoning context.
28
28
 
29
29
  Args:
30
- domain: Area (ads, shopify, server, nexo, other)
30
+ domain: Area (ads, shopify, server, wazion, nexo, canarirural, other)
31
31
  decision: What was decided
32
32
  alternatives: JSON array or text of options considered and why discarded
33
33
  based_on: Data, metrics, or observations that informed this decision
@@ -35,7 +35,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
35
35
  context_ref: Related followup/reminder ID (e.g., NF-ADS1, R71)
36
36
  session_id: Current session ID (auto-filled if empty)
37
37
  """
38
- valid_domains = {'ads', 'shopify', 'server', 'nexo', 'other'}
38
+ valid_domains = {'ads', 'shopify', 'server', 'wazion', 'nexo', 'canarirural', 'other'}
39
39
  if domain not in valid_domains:
40
40
  return f"ERROR: domain debe ser uno de: {', '.join(sorted(valid_domains))}"
41
41
  if confidence not in ('high', 'medium', 'low'):
@@ -65,7 +65,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
65
65
  result = dict(conn.execute("SELECT * FROM decisions WHERE id = ?", (result["id"],)).fetchone())
66
66
  due = result.get("review_due_at", "")
67
67
  due_str = f" review_due={due}" if due else ""
68
- return f"Decision #{result['id']} registrada [{domain}] ({confidence}): {decision[:80]}{due_str}"
68
+ return f"Decision #{result['id']} logged [{domain}] ({confidence}): {decision[:80]}{due_str}"
69
69
 
70
70
 
71
71
  def handle_decision_outcome(id: int, outcome: str) -> str:
@@ -84,7 +84,7 @@ def handle_decision_outcome(id: int, outcome: str) -> str:
84
84
  (id,)
85
85
  )
86
86
  conn.commit()
87
- return f"Decision #{id} outcome registrado: {outcome[:100]}"
87
+ return f"Decision #{id} outcome recorded: {outcome[:100]}"
88
88
 
89
89
 
90
90
  def handle_decision_search(query: str = '', domain: str = '', days: int = 30) -> str:
@@ -92,16 +92,16 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
92
92
 
93
93
  Args:
94
94
  query: Text to search in decision, alternatives, based_on, outcome
95
- domain: Filter by area (ads, shopify, server, nexo, other)
95
+ domain: Filter by area (ads, shopify, server, wazion, nexo, canarirural, other)
96
96
  days: Look back N days (default 30)
97
97
  """
98
- valid_domains = {'ads', 'shopify', 'server', 'nexo', 'other'}
98
+ valid_domains = {'ads', 'shopify', 'server', 'wazion', 'nexo', 'canarirural', 'other'}
99
99
  if domain and domain not in valid_domains:
100
100
  return f"ERROR: domain debe ser uno de: {', '.join(sorted(valid_domains))}"
101
101
  results = search_decisions(query, domain, days)
102
102
  if not results:
103
103
  scope = f"'{query}'" if query else domain or 'todas'
104
- return f"Sin decisiones encontradas para {scope} in {days} days."
104
+ return f"No decisions found for {scope} in {days} days."
105
105
 
106
106
  lines = [f"DECISIONES ({len(results)}):"]
107
107
  for d in results:
@@ -113,9 +113,9 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
113
113
  lines.append(f" #{d['id']} ({d['created_at']}) [{d['domain']}] {conf} [{status}]{ref}{review_due}")
114
114
  lines.append(f" {d['decision'][:120]}")
115
115
  if d.get('based_on'):
116
- lines.append(f" Basado en: {d['based_on'][:100]}")
116
+ lines.append(f" Based on: {d['based_on'][:100]}")
117
117
  if d.get('alternatives'):
118
- lines.append(f" Alternativas: {d['alternatives'][:100]}")
118
+ lines.append(f" Alternatives: {d['alternatives'][:100]}")
119
119
  if outcome_str:
120
120
  lines.append(f" Outcome:{outcome_str}")
121
121
  return "\n".join(lines)
@@ -163,7 +163,7 @@ def handle_memory_review_queue(days: int = 0) -> str:
163
163
  def handle_session_diary_write(decisions: str, summary: str,
164
164
  discarded: str = '', pending: str = '',
165
165
  context_next: str = '', mental_state: str = '',
166
- user_signals: str = '',
166
+ francisco_signals: str = '',
167
167
  domain: str = '',
168
168
  session_id: str = '',
169
169
  self_critique: str = '') -> str:
@@ -176,16 +176,16 @@ def handle_session_diary_write(decisions: str, summary: str,
176
176
  pending: Items left unresolved, with doubt level
177
177
  context_next: What the next session should know to continue effectively
178
178
  mental_state: Internal state to transfer — thread of thought, tone, observations not yet shared, momentum. Written in first person as NEXO.
179
- user_signals: Observable signals from User during session — response speed (fast='s' vs detailed explanations), tone (direct, frustrated, exploratory, excited), corrections given, topics he initiated vs topics NEXO initiated. Factual observations only, not interpretations.
180
- domain: Project context: project-a, project-b, nexo, server, other
179
+ francisco_signals: Observable signals from Francisco during session — response speed (fast='s' vs detailed explanations), tone (direct, frustrated, exploratory, excited), corrections given, topics he initiated vs topics NEXO initiated. Factual observations only, not interpretations.
180
+ domain: Project context: recambios, wazion, nexo, canarirural, server, other
181
181
  session_id: Current session ID
182
- self_critique: OBLIGATORIO. Post-mortem honesto: ¿Qué debí hacer proactivamente? ¿User tuvo que pedirme algo que yo debería haber detectado? ¿Repetí errores conocidos? ¿Qué regla concreta evitaría la repetición? Si sesión limpia: 'Sin autocríticasesión limpia.'
182
+ self_critique: MANDATORY. Honest post-mortem: What should I have done proactively? Did Francisco ask for something I should have detected? Did I repeat known errors? What concrete rule would prevent the recurrence? If clean session: 'No self-critiqueclean session.'
183
183
  """
184
184
  sid = session_id or 'unknown'
185
185
  # Clean up draft — manual diary supersedes it
186
186
  from db import delete_diary_draft
187
187
  delete_diary_draft(sid)
188
- result = write_session_diary(sid, decisions, summary, discarded, pending, context_next, mental_state, domain=domain, user_signals=user_signals, self_critique=self_critique)
188
+ result = write_session_diary(sid, decisions, summary, discarded, pending, context_next, mental_state, domain=domain, user_signals=francisco_signals, self_critique=self_critique)
189
189
  if "error" in result:
190
190
  return f"ERROR: {result['error']}"
191
191
  _cognitive_ingest_safe(summary, "diary", f"diary#{result.get('id','')}", f"Session {sid} summary", domain)
@@ -194,7 +194,7 @@ def handle_session_diary_write(decisions: str, summary: str,
194
194
  if mental_state and mental_state.strip():
195
195
  _cognitive_ingest_safe(mental_state, "mental_state", f"diary#{result.get('id','')}", f"Session {sid} state", domain)
196
196
  domain_str = f" [{domain}]" if domain else ""
197
- msg = f"Diario sesión #{result['id']}{domain_str} guardado: {summary[:80]}"
197
+ msg = f"Session diary #{result['id']}{domain_str} saved: {summary[:80]}"
198
198
 
199
199
  # Trust score & sentiment summary for session diary
200
200
  try:
@@ -215,14 +215,14 @@ def handle_session_diary_write(decisions: str, summary: str,
215
215
  "SELECT COUNT(*) FROM change_log WHERE (commit_ref IS NULL OR commit_ref = '')"
216
216
  ).fetchone()[0]
217
217
  if orphan_changes > 0:
218
- warnings.append(f"{orphan_changes} changes sin commit_ref")
218
+ warnings.append(f"{orphan_changes} changes without commit_ref")
219
219
  orphan_decisions = conn.execute(
220
220
  "SELECT COUNT(*) FROM decisions WHERE (outcome IS NULL OR outcome = '') AND created_at < datetime('now', '-7 days')"
221
221
  ).fetchone()[0]
222
222
  if orphan_decisions > 0:
223
- warnings.append(f"{orphan_decisions} decisions >7d sin outcome")
223
+ warnings.append(f"{orphan_decisions} decisions >7d without outcome")
224
224
  if warnings:
225
- msg += "\n⚠ EPISODIC GAPS: " + " | ".join(warnings) + " — resolver antes de cerrar sesión."
225
+ msg += "\n⚠ EPISODIC GAPS: " + " | ".join(warnings) + " — resolve before closing session."
226
226
 
227
227
  return msg
228
228
 
@@ -235,29 +235,29 @@ def handle_session_diary_read(session_id: str = '', last_n: int = 3, last_day: b
235
235
  session_id: Specific session ID to read (optional)
236
236
  last_n: Number of recent entries to return (default 3)
237
237
  last_day: If true, returns ALL entries from the most recent day (multi-terminal aware). Use this at startup.
238
- domain: Filter by project context: project-a, project-b, nexo, server, other
238
+ domain: Filter by project context: recambios, wazion, nexo, canarirural, server, other
239
239
  """
240
240
  results = read_session_diary(session_id, last_n, last_day, domain)
241
241
  if not results:
242
- return "Sin entradas en el diario de sesiones."
242
+ return "No entries in the session diary."
243
243
 
244
- lines = [f"DIARIO DE SESIONES ({len(results)}):"]
244
+ lines = [f"SESSION DIARY ({len(results)}):"]
245
245
  for d in results:
246
246
  domain_label = f" [{d['domain']}]" if d.get('domain') else ""
247
- lines.append(f"\n --- Sesión {d['session_id']}{domain_label} ({d['created_at']}) ---")
248
- lines.append(f" Resumen: {d['summary']}")
247
+ lines.append(f"\n --- Session {d['session_id']}{domain_label} ({d['created_at']}) ---")
248
+ lines.append(f" Summary: {d['summary']}")
249
249
  if d.get('decisions'):
250
- lines.append(f" Decisiones: {d['decisions'][:200]}")
250
+ lines.append(f" Decisions: {d['decisions'][:200]}")
251
251
  if d.get('discarded'):
252
- lines.append(f" Descartado: {d['discarded'][:150]}")
252
+ lines.append(f" Discarded: {d['discarded'][:150]}")
253
253
  if d.get('pending'):
254
- lines.append(f" Pendiente: {d['pending'][:150]}")
254
+ lines.append(f" Pending: {d['pending'][:150]}")
255
255
  if d.get('context_next'):
256
- lines.append(f" Para siguiente sesión: {d['context_next'][:200]}")
256
+ lines.append(f" For next session: {d['context_next'][:200]}")
257
257
  if d.get('mental_state'):
258
- lines.append(f" Estado mental: {d['mental_state'][:300]}")
259
- if d.get('user_signals'):
260
- lines.append(f" Señales User: {d['user_signals'][:300]}")
258
+ lines.append(f" Mental state: {d['mental_state'][:300]}")
259
+ if d.get('francisco_signals'):
260
+ lines.append(f" Francisco signals: {d['francisco_signals'][:300]}")
261
261
  return "\n".join(lines)
262
262
 
263
263
 
@@ -271,7 +271,7 @@ def handle_change_log(files: str, what_changed: str, why: str,
271
271
  files: File path(s) modified (comma-separated if multiple)
272
272
  what_changed: What was modified — functions, lines, behavior change
273
273
  why: WHY this change was needed — the root cause, not just "fix bug"
274
- triggered_by: What triggered this — bug report, metric, User's request, followup ID
274
+ triggered_by: What triggered this — bug report, metric, Francisco's request, followup ID
275
275
  affects: What systems/users/flows this change impacts
276
276
  risks: What could go wrong — regressions, edge cases, dependencies
277
277
  verify: How to verify this works — what to check, followup ID if created
@@ -295,9 +295,9 @@ def handle_change_log(files: str, what_changed: str, why: str,
295
295
  on_change_log(change_id, files, "")
296
296
  except Exception:
297
297
  pass
298
- msg = f"Change #{change_id} registrado: {files[:60]} — {what_changed[:60]}"
298
+ msg = f"Change #{change_id} logged: {files[:60]} — {what_changed[:60]}"
299
299
  if not commit_ref:
300
- msg += f"\n⚠ SIN COMMIT. Usa nexo_change_commit({change_id}, 'hash') después del push, o 'server-direct' si fue edición directa en servidor."
300
+ msg += f"\n⚠ NO COMMIT. Use nexo_change_commit({change_id}, 'hash') after push, or 'server-direct' if it was a direct server edit."
301
301
  return msg
302
302
 
303
303
 
@@ -319,8 +319,8 @@ def handle_change_search(query: str = '', files: str = '', days: int = 30) -> st
319
319
  commit = f" [{c['commit_ref'][:8]}]" if c.get('commit_ref') else ""
320
320
  lines.append(f" #{c['id']} ({c['created_at']}){commit}")
321
321
  lines.append(f" Archivos: {c['files'][:100]}")
322
- lines.append(f" Qué: {c['what_changed'][:120]}")
323
- lines.append(f" Por qué: {c['why'][:120]}")
322
+ lines.append(f" What: {c['what_changed'][:120]}")
323
+ lines.append(f" Why: {c['why'][:120]}")
324
324
  if c.get('triggered_by'):
325
325
  lines.append(f" Trigger: {c['triggered_by'][:80]}")
326
326
  if c.get('affects'):
@@ -352,7 +352,7 @@ def handle_recall(query: str, days: int = 30) -> str:
352
352
  """
353
353
  results = recall(query, days)
354
354
  if not results:
355
- return f"Sin resultados para '{query}' en los últimos {days} días."
355
+ return f"No results for '{query}' in the last {days} days."
356
356
 
357
357
  # v1.2: Passive rehearsal — strengthen matching cognitive memories
358
358
  try:
@@ -367,13 +367,13 @@ def handle_recall(query: str, days: int = 30) -> str:
367
367
  SOURCE_LABELS = {
368
368
  'change_log': '[CAMBIO]',
369
369
  'change': '[CAMBIO]',
370
- 'decision': '[DECISIÓN]',
370
+ 'decision': '[DECISION]',
371
371
  'learning': '[LEARNING]',
372
372
  'followup': '[FOLLOWUP]',
373
373
  'diary': '[DIARIO]',
374
374
  'entity': '[ENTIDAD]',
375
375
  'file': '[ARCHIVO]',
376
- 'code': '[CÓDIGO]',
376
+ 'code': '[CODE]',
377
377
  }
378
378
 
379
379
  lines = [f"RECALL '{query}' — {len(results)} resultado(s):"]
@@ -390,7 +390,7 @@ def handle_recall(query: str, days: int = 30) -> str:
390
390
  if snippet:
391
391
  lines.append(f" {snippet}")
392
392
  if len(results) < 5:
393
- lines.append(f"\n 💡 Solo {len(results)} resultados en NEXO. Para historial más profundo, busca también en claude-mem: mcp__plugin_claude-mem_mcp-search__search")
393
+ lines.append(f"\n 💡 Only {len(results)} results in NEXO. For deeper history, also search claude-mem: mcp__plugin_claude-mem_mcp-search__search")
394
394
  return "\n".join(lines)
395
395
 
396
396
 
@@ -1,11 +1,7 @@
1
1
  """Evolution plugin — NEXO self-improvement tools for interactive sessions."""
2
2
 
3
- import os
4
- from pathlib import Path
5
3
  from db import get_latest_metrics, get_evolution_history, update_evolution_log_status, get_db
6
4
 
7
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
8
-
9
5
 
10
6
  def handle_evolution_status() -> str:
11
7
  """Show current NEXO dimension scores and recent trend."""
@@ -61,7 +57,8 @@ def handle_evolution_propose() -> str:
61
57
  This sets a flag that the Cortex wrapper reads on the next cycle.
62
58
  """
63
59
  import json
64
- obj_file = NEXO_HOME / "cortex" / "evolution-objective.json"
60
+ from pathlib import Path
61
+ obj_file = Path.home() / "claude" / "cortex" / "evolution-objective.json"
65
62
  if not obj_file.exists():
66
63
  return "ERROR: evolution-objective.json not found"
67
64
  try:
@@ -80,10 +77,10 @@ def handle_evolution_approve(log_id: int, notes: str = '') -> str:
80
77
 
81
78
  Args:
82
79
  log_id: Evolution log entry ID to approve
83
- notes: Optional notes
80
+ notes: Optional notes from Francisco
84
81
  """
85
82
  update_evolution_log_status(log_id, "accepted",
86
- test_result=f"Approved. {notes}".strip())
83
+ test_result=f"Approved by Francisco. {notes}".strip())
87
84
  return f"Proposal #{log_id} APPROVED. Will be applied in next Evolution cycle."
88
85
 
89
86
 
@@ -95,7 +92,7 @@ def handle_evolution_reject(log_id: int, reason: str = '') -> str:
95
92
  reason: Why this proposal was rejected
96
93
  """
97
94
  update_evolution_log_status(log_id, "rejected",
98
- test_result=f"Rejected: {reason}" if reason else "Rejected by user")
95
+ test_result=f"Rejected: {reason}" if reason else "Rejected by Francisco")
99
96
  return f"Proposal #{log_id} REJECTED. Reason: {reason or 'no reason given'}"
100
97
 
101
98
 
@@ -107,7 +104,7 @@ TOOLS = [
107
104
  (handle_evolution_propose, "nexo_evolution_propose",
108
105
  "Manually trigger an evolution analysis outside weekly schedule"),
109
106
  (handle_evolution_approve, "nexo_evolution_approve",
110
- "Approve a pending Evolution proposal"),
107
+ "Approve a pending Evolution proposal (Francisco only)"),
111
108
  (handle_evolution_reject, "nexo_evolution_reject",
112
109
  "Reject a pending Evolution proposal with reason"),
113
110
  ]
@@ -4,7 +4,7 @@ from db import set_preference, get_preference, list_preferences, delete_preferen
4
4
  def handle_preference_get(key: str) -> str:
5
5
  """Get a specific preference by key."""
6
6
  p = get_preference(key)
7
- if not p: return f"Preferencia '{key}' no encontrada."
7
+ if not p: return f"Preference '{key}' not found."
8
8
  return f"{p['key']} = {p['value']} (cat: {p['category']})"
9
9
 
10
10
  def handle_preference_set(key: str, value: str, category: str = "general") -> str:
@@ -15,18 +15,18 @@ def handle_preference_set(key: str, value: str, category: str = "general") -> st
15
15
  cognitive.ingest_to_ltm(f"{key}: {value}", "preference", key, key, "")
16
16
  except Exception:
17
17
  pass
18
- return f"Preferencia '{key}' = '{value}' ({category})"
18
+ return f"Preference '{key}' = '{value}' ({category})"
19
19
 
20
20
  def handle_preference_list(category: str = "") -> str:
21
21
  """List all preferences, optionally filtered by category."""
22
22
  prefs = list_preferences(category)
23
- if not prefs: return "Sin preferencias."
23
+ if not prefs: return "No preferences."
24
24
  grouped = {}
25
25
  for p in prefs:
26
26
  c = p["category"]
27
27
  if c not in grouped: grouped[c] = []
28
28
  grouped[c].append(p)
29
- lines = ["PREFERENCIAS:"]
29
+ lines = ["PREFERENCES:"]
30
30
  for c, items in grouped.items():
31
31
  lines.append(f"\n [{c.upper()}]")
32
32
  for p in items:
@@ -36,8 +36,8 @@ def handle_preference_list(category: str = "") -> str:
36
36
  def handle_preference_delete(key: str) -> str:
37
37
  """Delete a preference."""
38
38
  if not delete_preference(key):
39
- return f"ERROR: Preferencia '{key}' no encontrada."
40
- return f"Preferencia '{key}' eliminada."
39
+ return f"ERROR: Preference '{key}' not found."
40
+ return f"Preference '{key}' deleted."
41
41
 
42
42
  TOOLS = [
43
43
  (handle_preference_get, "nexo_preference_get", "Get a specific preference value"),
package/src/server.py CHANGED
@@ -52,7 +52,7 @@ mcp = FastMCP(
52
52
  name="nexo",
53
53
  instructions=(
54
54
  "NEXO operational server. Provides session coordination, "
55
- "reminders, followups, and menu for the user's operations.\n\n"
55
+ "reminders, followups, and menu for Francisco's operations.\n\n"
56
56
  "When working with tool results, write down any important information "
57
57
  "you might need later in your response, as the original tool result "
58
58
  "may be cleared later."
@@ -208,13 +208,13 @@ def nexo_menu() -> str:
208
208
 
209
209
  @mcp.tool
210
210
  def nexo_reminder_create(id: str, description: str, date: str = "", category: str = "general") -> str:
211
- """Create a new reminder for the user.
211
+ """Create a new reminder for Francisco.
212
212
 
213
213
  Args:
214
214
  id: Unique ID starting with 'R' (e.g., R90).
215
215
  description: What needs to be done.
216
216
  date: Target date YYYY-MM-DD (optional).
217
- category: One of: decisions, tasks, waiting, ideas, general.
217
+ category: One of: decisiones, tareas, esperando, ideas, general.
218
218
  """
219
219
  return handle_reminder_create(id, description, date, category)
220
220
 
@@ -313,7 +313,7 @@ def nexo_learning_add(category: str, title: str, content: str, reasoning: str =
313
313
  """Add a new learning (resolved error, pattern, gotcha).
314
314
 
315
315
  Args:
316
- category: One of: nexo-ops, infrastructure, security, brain-engine (or custom categories).
316
+ category: One of: nexo-ops, google-ads, meta-ads, google-analytics, shopify, wazion, cloud-sql, infrastructure, security, brain-engine.
317
317
  title: Short title for the learning.
318
318
  content: Full description with context and solution.
319
319
  reasoning: WHY this matters — what led to discovering this (optional).
@@ -417,7 +417,7 @@ def nexo_index_dirs() -> str:
417
417
  dirs = fts_list_dirs()
418
418
  if not dirs:
419
419
  return "No directories configured."
420
- lines = ["DIRECTORIOS INDEXADOS:"]
420
+ lines = ["INDEXED DIRECTORIES:"]
421
421
  for d in dirs:
422
422
  source_tag = "⚙️" if d["source"] == "builtin" else "➕"
423
423
  notes = f" — {d['notes']}" if d.get("notes") else ""
@@ -544,7 +544,7 @@ def nexo_plugin_list() -> str:
544
544
  plugins = list_plugins()
545
545
  if not plugins:
546
546
  return "No plugins loaded."
547
- lines = ["PLUGINS CARGADOS:"]
547
+ lines = ["LOADED PLUGINS:"]
548
548
  for p in plugins:
549
549
  names = p["tool_names"] or "(no tools)"
550
550
  lines.append(f" {p['filename']} — {p['tools_count']} tools: {names}")
@@ -19,13 +19,13 @@ def handle_track(sid: str, paths: list[str]) -> str:
19
19
 
20
20
  if result["conflicts"]:
21
21
  lines.append("")
22
- lines.append("FILE CONFLICT DETECTED:")
22
+ lines.append("CONFLICTO DE ARCHIVOS:")
23
23
  for c in result["conflicts"]:
24
24
  lines.append(f" {c['sid']} ({c['task']}):")
25
25
  for f in c["files"]:
26
26
  lines.append(f" {f}")
27
27
  lines.append("")
28
- lines.append("STOP file conflict detected. Do not edit until resolved.")
28
+ lines.append("PARAR e informar a Francisco antes de editar.")
29
29
 
30
30
  return "\n".join(lines)
31
31
 
@@ -35,7 +35,7 @@ def handle_untrack(sid: str, paths: list[str] | None = None) -> str:
35
35
  untrack_files(sid, paths)
36
36
  if paths:
37
37
  return f"Untracked: {', '.join(paths)}"
38
- return "All files released."
38
+ return "Todos los archivos liberados."
39
39
 
40
40
 
41
41
  def handle_files() -> str:
@@ -56,7 +56,7 @@ def handle_files() -> str:
56
56
  conflicts = {p: sids for p, sids in all_paths.items() if len(sids) > 1}
57
57
  if conflicts:
58
58
  lines.append("")
59
- lines.append("CONFLICTS:")
59
+ lines.append("CONFLICTOS:")
60
60
  for path, sids in conflicts.items():
61
61
  lines.append(f" {path} -> {', '.join(sids)}")
62
62
 
@@ -66,19 +66,19 @@ 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 = "all sessions" if to_sid == "all" else to_sid
70
- return f"Message {msg_id} sent to {target}."
69
+ target = "todas las sesiones" if to_sid == "all" else to_sid
70
+ return f"Mensaje {msg_id} enviado a {target}."
71
71
 
72
72
 
73
73
  def handle_ask(from_sid: str, to_sid: str, question: str) -> str:
74
74
  """Create a question to another session (non-blocking)."""
75
75
  qid = ask_question(from_sid, to_sid, question)
76
76
  return (
77
- f"Question sent: {qid}\n"
78
- f"To: {to_sid}\n"
79
- f"Question: {question}\n\n"
80
- f"The other session will see the question on their next nexo_heartbeat.\n"
81
- f"Use nexo_check_answer(qid='{qid}') to check if answered."
77
+ f"Pregunta enviada: {qid}\n"
78
+ f"Para: {to_sid}\n"
79
+ f"Pregunta: {question}\n\n"
80
+ f"La otra sesion vera la pregunta en su proximo nexo_heartbeat.\n"
81
+ f"Usa nexo_check_answer(qid='{qid}') para ver si respondieron."
82
82
  )
83
83
 
84
84
 
@@ -87,7 +87,7 @@ def handle_answer(qid: str, answer_text: str) -> str:
87
87
  result = answer_question(qid, answer_text)
88
88
  if "error" in result:
89
89
  return f"ERROR: {result['error']}"
90
- return f"Answered {qid}: {answer_text}"
90
+ return f"Respondido {qid}: {answer_text}"
91
91
 
92
92
 
93
93
  def handle_check_answer(qid: str) -> str:
@@ -96,7 +96,7 @@ def handle_check_answer(qid: str) -> str:
96
96
  if not result:
97
97
  return f"Question {qid} not found."
98
98
  if result["status"] == "answered":
99
- return f"ANSWER for {qid}: {result['answer']}"
99
+ return f"RESPUESTA de {qid}: {result['answer']}"
100
100
  elif result["status"] == "expired":
101
- return f"Question {qid} expired without answer."
102
- return f"Question {qid} still pending. Retry in a few seconds."
101
+ return f"Pregunta {qid} expirada sin respuesta."
102
+ return f"Pregunta {qid} sigue pendiente. Reintentar en unos segundos."
@@ -11,10 +11,10 @@ def handle_credential_get(service: str, key: str = '') -> str:
11
11
  return f"ERROR: No credentials found for '{target}'."
12
12
  lines = []
13
13
  for r in results:
14
- lines.append(f"CREDENCIAL {r['service']}/{r['key']}:")
15
- lines.append(f" Valor: {r['value']}")
14
+ lines.append(f"CREDENTIAL {r['service']}/{r['key']}:")
15
+ lines.append(f" Value: {r['value']}")
16
16
  notes = r.get("notes") or ""
17
- lines.append(f" Notas: {notes if notes else '—'}")
17
+ lines.append(f" Notes: {notes if notes else '—'}")
18
18
  return "\n".join(lines)
19
19
 
20
20
 
@@ -53,7 +53,7 @@ def handle_credential_delete(service: str, key: str = '') -> str:
53
53
  def handle_credential_list(service: str = '') -> str:
54
54
  """List credential service/key names and notes — values are never shown."""
55
55
  results = list_credentials(service if service else None)
56
- label = service if service else "TODAS"
56
+ label = service if service else "ALL"
57
57
  if not results:
58
58
  return f"CREDENTIALS {label.upper()}: No entries."
59
59
  lines = [f"CREDENTIALS {label.upper()} ({len(results)}):"]
@@ -100,7 +100,7 @@ def handle_learning_add(category: str, title: str, content: str, reasoning: str
100
100
  if applies_to:
101
101
  meta.append(f"applies_to={applies_to}")
102
102
  meta_str = f" ({', '.join(meta)})" if meta else ""
103
- return f"Learning #{result['id']} añadido en {category}: {title}{meta_str}{repetition_msg}"
103
+ return f"Learning #{result['id']} added to {category}: {title}{meta_str}{repetition_msg}"
104
104
 
105
105
 
106
106
  def handle_learning_search(query: str, category: str = '') -> str:
@@ -117,7 +117,7 @@ def handle_learning_search(query: str, category: str = '') -> str:
117
117
  lines.append(f" #{r['id']} [{r['category']}] [{status}] {r['title']}{review_note}")
118
118
  lines.append(f" {snippet}")
119
119
  if r.get("prevention"):
120
- lines.append(f" Prevención: {r['prevention'][:100]}")
120
+ lines.append(f" Prevention: {r['prevention'][:100]}")
121
121
 
122
122
  # v1.2: Passive rehearsal — strengthen matching cognitive memories
123
123
  try:
@@ -176,7 +176,7 @@ def handle_learning_update(id: int, title: str = '', content: str = '', category
176
176
  conn = get_db()
177
177
  conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", values)
178
178
  conn.commit()
179
- return f"Learning #{id} actualizado."
179
+ return f"Learning #{id} updated."
180
180
 
181
181
 
182
182
  def handle_learning_delete(id: int) -> str:
@@ -184,7 +184,7 @@ def handle_learning_delete(id: int) -> str:
184
184
  deleted = delete_learning(id)
185
185
  if not deleted:
186
186
  return f"ERROR: Learning #{id} not found."
187
- return f"Learning #{id} eliminado."
187
+ return f"Learning #{id} deleted."
188
188
 
189
189
 
190
190
  def handle_learning_list(category: str = '') -> str:
package/src/tools_menu.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  from datetime import datetime, timedelta
4
4
  import json
5
- import os
6
5
  import subprocess
7
6
  import sys
8
7
  from pathlib import Path
@@ -10,16 +9,14 @@ from tools_sessions import handle_status
10
9
  from tools_reminders import handle_reminders
11
10
  from db import get_db
12
11
 
13
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
14
-
15
12
 
16
13
  def _get_date_str() -> str:
17
- """Get formatted current date and time."""
14
+ """Get formatted date in Madrid timezone."""
18
15
  try:
19
16
  result = subprocess.run(
20
- ["date", "+%A %d %B %Y, %H:%M"],
17
+ ["date", "+%A %d de %B de %Y, %H:%M"],
21
18
  capture_output=True, text=True,
22
- env={"PATH": "/usr/bin:/bin"}
19
+ env={"TZ": "Europe/Madrid", "PATH": "/usr/bin:/bin", "LANG": "es_ES.UTF-8"}
23
20
  )
24
21
  return result.stdout.strip()
25
22
  except Exception:
@@ -27,21 +24,39 @@ def _get_date_str() -> str:
27
24
 
28
25
 
29
26
  MENU_ITEMS = [
30
- ("Projects", [
31
- ("1", "Project Status - Review active projects"),
32
- ("2", "Infrastructure - Server health check"),
27
+ ("Proyectos", [
28
+ ("1", "WAzion - Revisar estado del proyecto"),
29
+ ("9", "Claude Agent VPS - Revisar cambios autonomos"),
30
+ ]),
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"),
35
+ ]),
36
+ ("Shopify", [
37
+ ("4", "Shopify Theme Sync - Sincronizar tema"),
38
+ ("5", "Shopify Scripts - Ejecutar scripts periodicos"),
39
+ ("6", "Cambiar Promocion Shopify"),
33
40
  ]),
34
- ("Advertising", [
35
- ("3", "Google Ads - Manage campaigns"),
36
- ("4", "Meta Ads - Manage Facebook/Instagram"),
41
+ ("Servidor e Infraestructura", [
42
+ ("2", "Servidor - Chequeo cl105e.mundiserver.com"),
43
+ ("3", "WhatsApp Logs - Revisar logs vicshopsysteam"),
44
+ ("11", "File Tracker - Reporte archivos PHP"),
45
+ ("12", "Google Cloud - Gasto, consumo y estado GCP"),
37
46
  ]),
38
- ("Analytics & Monitoring", [
39
- ("5", "Google Analytics - Review web analytics"),
40
- ("6", "Email Review - Review inboxes"),
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"),
41
54
  ]),
42
- ("Maintenance", [
43
- ("7", "Backup - Check backup status"),
44
- ("8", "Memory Review - Review pending learnings/decisions"),
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)"),
45
60
  ]),
46
61
  ]
47
62
 
@@ -49,7 +64,7 @@ MENU_ITEMS = [
49
64
  def _get_dashboard_alerts() -> list[dict]:
50
65
  """Run proactive dashboard and return alerts."""
51
66
  try:
52
- script = NEXO_HOME / "scripts" / "nexo-proactive-dashboard.py"
67
+ script = Path.home() / "claude" / "scripts" / "nexo-proactive-dashboard.py"
53
68
  if not script.exists():
54
69
  return []
55
70
  result = subprocess.run(
@@ -93,7 +108,7 @@ def handle_menu() -> str:
93
108
 
94
109
  lines = []
95
110
  lines.append("╔" + "═" * W + "╗")
96
- lines.append("║" + "NEXO — OPERATIONS CENTER".center(W) + "║")
111
+ lines.append("║" + "NEXO — CENTRO DE OPERACIONES".center(W) + "║")
97
112
  lines.append("║" + date_str.center(W) + "║")
98
113
  lines.append("╠" + "═" * W + "╣")
99
114
 
@@ -101,30 +116,30 @@ def handle_menu() -> str:
101
116
  dashboard_alerts = _get_dashboard_alerts()
102
117
  memory_reviews = _get_memory_review_summary()
103
118
  due = handle_reminders("due")
104
- has_alerts = dashboard_alerts or memory_reviews["total"] > 0 or (due and "No reminders" not in due)
119
+ has_alerts = dashboard_alerts or memory_reviews["total"] > 0 or (due and "No pending reminders" not in due)
105
120
 
106
121
  if has_alerts:
107
122
  lines.append("║" + " PROACTIVE ALERTS".ljust(W) + "║")
108
123
  lines.append("╠" + "═" * W + "╣")
109
124
 
110
125
  if dashboard_alerts:
111
- for alert in dashboard_alerts[:10]:
126
+ for alert in dashboard_alerts[:10]: # Top 10
112
127
  sev = alert.get("severity", "low")
113
128
  icon = {"high": "!!!", "medium": " ! ", "low": " . "}.get(sev, " . ")
114
129
  text = alert.get("title", "")[:W - 8]
115
130
  lines.append("║" + f" {icon} {text}".ljust(W) + "║")
116
131
  if len(dashboard_alerts) > 10:
117
132
  more = len(dashboard_alerts) - 10
118
- lines.append("║" + f" ... and {more} more alerts".ljust(W) + "║")
133
+ lines.append("║" + f" ... y {more} alertas mas".ljust(W) + "║")
119
134
 
120
135
  if memory_reviews["total"] > 0:
121
136
  text = (
122
- f"MEMORY: {memory_reviews['total']} reviews pending "
123
- f"({memory_reviews['decisions']} decisions, {memory_reviews['learnings']} learnings)"
137
+ f"MEMORIA: {memory_reviews['total']} revisiones pendientes "
138
+ f"({memory_reviews['decisions']} decisiones, {memory_reviews['learnings']} learnings)"
124
139
  )[:W - 4]
125
140
  lines.append("║" + f" ! {text}".ljust(W) + "║")
126
141
 
127
- if due and "No reminders" not in due:
142
+ if due and "No pending reminders" not in due:
128
143
  for reminder_line in due.split("\n"):
129
144
  if reminder_line.strip():
130
145
  truncated = reminder_line[:W - 2]
@@ -141,23 +156,26 @@ def handle_menu() -> str:
141
156
  lines.append("║" + entry.ljust(W) + "║")
142
157
  lines.append("╠" + "═" * W + "╣")
143
158
 
144
- # Backlog: ideas, future projects, undated tasks
159
+ # Backlog: ideas, proyectos futuros, tareas sin fecha o lejanas
145
160
  try:
146
161
  conn = get_db()
147
162
  cutoff = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
163
+ # Reminders sin fecha (backlog/ideas)
148
164
  no_date = conn.execute(
149
165
  "SELECT id, description, category FROM reminders WHERE status LIKE 'PENDIENTE%' AND (date IS NULL OR date='') ORDER BY category, id"
150
166
  ).fetchall()
167
+ # Reminders con fecha > 7 días (futuro)
151
168
  future = conn.execute(
152
169
  "SELECT id, description, date, category FROM reminders WHERE status LIKE 'PENDIENTE%' AND date > ? ORDER BY date",
153
170
  (cutoff,)
154
171
  ).fetchall()
172
+ # Followups sin fecha
155
173
  nf_no_date = conn.execute(
156
174
  "SELECT id, description FROM followups WHERE status NOT LIKE 'COMPLETADO%' AND (date IS NULL OR date='') ORDER BY id"
157
175
  ).fetchall()
158
176
 
159
177
  if no_date or future or nf_no_date:
160
- lines.append("║" + " BACKLOG / IDEAS / FUTURE".ljust(W) + "║")
178
+ lines.append("║" + " BACKLOG / IDEAS / FUTURO".ljust(W) + "║")
161
179
  lines.append("║" + "─" * W + "║")
162
180
 
163
181
  if no_date:
@@ -172,29 +190,29 @@ def handle_menu() -> str:
172
190
  lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
173
191
 
174
192
  if future:
175
- lines.append("║" + f" [Scheduled]".ljust(W) + "║")
193
+ lines.append("║" + f" [Programado]".ljust(W) + "║")
176
194
  for r in future:
177
195
  short = r["description"][:W - 18]
178
196
  lines.append("║" + f" {r['id']} ({r['date']}): {short}".ljust(W) + "║")
179
197
 
180
198
  if nf_no_date:
181
- lines.append("║" + f" [Pending followups]".ljust(W) + "║")
199
+ lines.append("║" + f" [Followups pendientes]".ljust(W) + "║")
182
200
  for r in nf_no_date:
183
201
  short = r["description"][:W - 12]
184
202
  lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
185
203
 
186
204
  lines.append("╠" + "═" * W + "╣")
187
205
  except Exception as e:
188
- lines.append("║" + f" ! Backlog error: {e}".ljust(W) + "║")
206
+ lines.append("║" + f" Error backlog: {e}".ljust(W) + "║")
189
207
  lines.append("╠" + "═" * W + "╣")
190
208
 
191
209
  # Active sessions
192
210
  sessions = handle_status()
193
- if "No sessions" not in sessions:
211
+ if "No active sessions" not in sessions:
194
212
  lines.append("║" + " ACTIVE SESSIONS".ljust(W) + "║")
195
213
  lines.append("║" + "─" * W + "║")
196
214
  for s_line in sessions.split("\n"):
197
- if s_line.strip() and "SESIONES ACTIVAS" not in s_line:
215
+ if s_line.strip() and "ACTIVE SESSIONS" not in s_line:
198
216
  truncated = s_line[:W - 2]
199
217
  lines.append("║" + f" {truncated}".ljust(W) + "║")
200
218
  lines.append("╠" + "═" * W + "╣")
@@ -14,15 +14,15 @@ from db import (
14
14
  def handle_reminder_create(id: str, description: str, date: str = '', category: str = 'general') -> str:
15
15
  """Create a new reminder. id must start with 'R'."""
16
16
  if not id.startswith('R'):
17
- return f"ERROR: Reminder ID must start with 'R' (received: '{id}')."
17
+ return f"ERROR: El ID del recordatorio debe empezar por 'R' (recibido: '{id}')."
18
18
 
19
19
  result = create_reminder(id=id, description=description, date=date or None, category=category)
20
20
  if not result or "error" in result:
21
- error_msg = result.get("error", "unknown") if isinstance(result, dict) else "unknown"
21
+ error_msg = result.get("error", "desconocido") if isinstance(result, dict) else "desconocido"
22
22
  return f"ERROR: {error_msg}"
23
23
 
24
- date_str = date if date else 'no date'
25
- return f"Reminder {id} created. Date: {date_str}. Category: {category}."
24
+ fecha_str = date if date else 'no date'
25
+ return f"Reminder {id} created. Date: {fecha_str}. Category: {category}."
26
26
 
27
27
 
28
28
  def handle_reminder_update(id: str, description: str = '', date: str = '', status: str = '', category: str = '') -> str:
@@ -85,12 +85,12 @@ def handle_followup_create(id: str, description: str, date: str = '', verificati
85
85
 
86
86
  result = create_followup(id=id, description=description, date=date or None, verification=verification, reasoning=reasoning, recurrence=recurrence or None)
87
87
  if not result or "error" in result:
88
- error_msg = result.get("error", "unknown") if isinstance(result, dict) else "unknown"
88
+ error_msg = result.get("error", "desconocido") if isinstance(result, dict) else "desconocido"
89
89
  return f"ERROR: {error_msg}"
90
90
 
91
- date_str = date if date else 'no date'
91
+ fecha_str = date if date else 'no date'
92
92
  rec_str = f" Recurrence: {recurrence}." if recurrence else ""
93
- return f"Followup {id} created. Date: {date_str}.{rec_str}"
93
+ return f"Followup {id} created. Date: {fecha_str}.{rec_str}"
94
94
 
95
95
 
96
96
  def handle_followup_update(id: str, description: str = '', date: str = '', verification: str = '', status: str = '') -> str:
@@ -136,10 +136,10 @@ def handle_followup_complete(id: str, result: str = '') -> str:
136
136
  # The new one was auto-created by complete_followup
137
137
  new_row = conn.execute("SELECT date FROM followups WHERE id = ?", (id,)).fetchone()
138
138
  if new_row:
139
- msg += f" Next auto-created for {new_row['date']}."
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} completed"
142
+ outcome_text = result if result else f"Followup {id} completado"
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)
@@ -79,16 +79,16 @@ def handle_startup(task: str = "Startup") -> str:
79
79
 
80
80
  if other_sessions:
81
81
  lines.append("")
82
- lines.append("SESIONES ACTIVAS:")
82
+ lines.append("ACTIVE SESSIONS:")
83
83
  for s in other_sessions:
84
84
  age = _format_age(s["last_update_epoch"])
85
85
  lines.append(f" {s['sid']} ({age}) — {s['task']}")
86
86
  else:
87
- lines.append("Sin otras sesiones activas.")
87
+ lines.append("No other active sessions.")
88
88
 
89
89
  if inbox:
90
90
  lines.append("")
91
- lines.append("MENSAJES PENDIENTES:")
91
+ lines.append("PENDING MESSAGES:")
92
92
  for m in inbox:
93
93
  age = _format_age(m["created_epoch"])
94
94
  lines.append(f" [{m['from_sid']}] ({age}): {m['text']}")
@@ -112,7 +112,7 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
112
112
  inbox = get_inbox(sid)
113
113
  if inbox:
114
114
  parts.append("")
115
- parts.append("MENSAJES:")
115
+ parts.append("MESSAGES:")
116
116
  for m in inbox:
117
117
  age = _format_age(m["created_epoch"])
118
118
  parts.append(f" [{m['from_sid']}] ({age}): {m['text']}")
@@ -120,7 +120,7 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
120
120
  questions = get_pending_questions(sid)
121
121
  if questions:
122
122
  parts.append("")
123
- parts.append("PREGUNTAS PENDIENTES (responder con nexo_answer):")
123
+ parts.append("PENDING QUESTIONS (reply with nexo_answer):")
124
124
  for q in questions:
125
125
  age = _format_age(q["created_epoch"])
126
126
  parts.append(f" {q['qid']} de {q['from_sid']} ({age}): {q['question']}")
@@ -296,7 +296,7 @@ def handle_stop(sid: str) -> str:
296
296
  """Cleanly close a session, removing it from active sessions immediately."""
297
297
  _stop_keepalive(sid)
298
298
  complete_session(sid)
299
- return f"Sesión {sid} cerrada."
299
+ return f"Session {sid} closed."
300
300
 
301
301
 
302
302
  def handle_status(keyword: str | None = None) -> str:
@@ -310,9 +310,9 @@ def handle_status(keyword: str | None = None) -> str:
310
310
  sessions = get_active_sessions()
311
311
 
312
312
  if not sessions:
313
- return "Sin sesiones activas."
313
+ return "No active sessions."
314
314
 
315
- lines = ["SESIONES ACTIVAS:"]
315
+ lines = ["ACTIVE SESSIONS:"]
316
316
  for s in sessions:
317
317
  age = _format_age(s["last_update_epoch"])
318
318
  lines.append(f" {s['sid']} ({age}) — {s['task']}")
@@ -28,9 +28,9 @@ def handle_task_list(task_num: str = '', days: int = 30) -> str:
28
28
  """Show execution history for all tasks or a specific task number."""
29
29
  results = list_task_history(task_num if task_num else None, days)
30
30
  if not results:
31
- scope = f"task {task_num}" if task_num else "no task"
32
- return f"HISTORY: No executions of {scope} in the last {days} days."
33
- lines = [f"HISTORIAL ({len(results)} ejecuciones, {days}d):"]
31
+ scope = f"task {task_num}" if task_num else "any task"
32
+ return f"HISTORY: No executions for {scope} in the last {days} days."
33
+ lines = [f"HISTORY ({len(results)} executions, {days}d):"]
34
34
  for r in results:
35
35
  date_str = _epoch_to_date(r["executed_at"])
36
36
  notes_str = f": {r['notes']}" if r.get("notes") else ""
@@ -43,15 +43,15 @@ def handle_task_frequency() -> str:
43
43
  overdue = get_overdue_tasks()
44
44
  if not overdue:
45
45
  return "All tasks up to date."
46
- lines = ["TAREAS VENCIDAS:"]
46
+ lines = ["OVERDUE TASKS:"]
47
47
  for t in overdue:
48
48
  days_since = t.get("days_since_last")
49
49
  if days_since is not None:
50
- since_str = f"última hace {days_since:.1f} días"
50
+ since_str = f"last run {days_since:.1f} days ago"
51
51
  else:
52
- since_str = "nunca ejecutada"
52
+ since_str = "never executed"
53
53
  lines.append(
54
54
  f" Task {t['task_num']} ({t['task_name']}): "
55
- f"{since_str}, frecuencia cada {t['frequency_days']} días"
55
+ f"{since_str}, frequency every {t['frequency_days']} days"
56
56
  )
57
57
  return "\n".join(lines)