nexo-brain 0.8.0 → 0.8.2

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
@@ -332,7 +332,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
332
332
  | Component | What | Where |
333
333
  |-----------|------|-------|
334
334
  | Cognitive engine | Python: fastembed, numpy, vector search | pip packages |
335
- | MCP server | 105+ tools for memory, cognition, learning, guard | ~/.nexo/ |
335
+ | MCP server | 109+ tools for memory, cognition, learning, guard | ~/.nexo/ |
336
336
  | Plugins | Guard, episodic memory, cognitive memory, entities, preferences | ~/.nexo/plugins/ |
337
337
  | Hooks (5) | SessionStart briefing, Stop post-mortem, PostToolUse capture, PreCompact checkpoint, Caffeinate | ~/.nexo/hooks/ |
338
338
  | Reflection engine | Processes session buffer, extracts patterns, updates user model | ~/.nexo/scripts/ |
@@ -345,7 +345,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
345
345
 
346
346
  - **macOS, Linux, or Windows**
347
347
  - **Node.js 18+** (for the installer)
348
- - **Claude Opus (latest version) strongly recommended.** NEXO Brain provides 109+ MCP tools across 17 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 105+ tools without hesitation.
348
+ - **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
349
  - Python 3, Homebrew, and Claude Code are installed automatically if missing.
350
350
 
351
351
  ## Architecture
@@ -372,7 +372,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
372
372
  | Agents | 5 | get, create, update, delete, list | Agent delegation registry |
373
373
  | Backup | 3 | now, list, restore | SQLite data safety |
374
374
  | Evolution | 5 | propose, approve, reject, status, history | Self-improvement proposals |
375
- | Adaptive & Somatic (4) | nexo_adaptive_weights, nexo_adaptive_override, nexo_somatic_check, nexo_somatic_stats |
375
+ | Adaptive & Somatic | 4 | adaptive_weights, adaptive_override, somatic_check, somatic_stats | Learned signal weights + pain memory per file |
376
376
  | Knowledge Graph | 4 | kg_query, kg_path, kg_neighbors, kg_stats | Bi-temporal entity-relationship graph |
377
377
 
378
378
  ### Plugin System
@@ -431,7 +431,7 @@ NEXO Brain is designed as an MCP server. Claude Code is the primary supported cl
431
431
  npx nexo-brain
432
432
  ```
433
433
 
434
- All 105+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically.
434
+ All 109+ tools are available immediately after installation. The installer configures Claude Code's `~/.claude/settings.json` automatically.
435
435
 
436
436
  ### OpenClaw
437
437
 
package/bin/nexo-brain.js CHANGED
@@ -211,27 +211,37 @@ async function main() {
211
211
  }
212
212
  }
213
213
 
214
- // Find or install Homebrew (needed for Python)
215
- let hasBrew = run("which brew");
216
- if (!hasBrew) {
217
- log("Homebrew not found. Installing...");
218
- spawnSync("/bin/bash", ["-c", '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'], {
219
- stdio: "inherit",
220
- });
221
- hasBrew = run("which brew") || run("eval $(/opt/homebrew/bin/brew shellenv) && which brew");
222
- }
223
-
224
- // Find or install Python
214
+ // Find or install Python (platform-aware)
225
215
  let python = run("which python3");
226
216
  if (!python) {
227
- if (hasBrew) {
228
- log("Python 3 not found. Installing via Homebrew...");
229
- spawnSync("brew", ["install", "python3"], { stdio: "inherit" });
217
+ if (platform === "darwin") {
218
+ // macOS: use Homebrew
219
+ let hasBrew = run("which brew");
220
+ if (!hasBrew) {
221
+ log("Homebrew not found. Installing...");
222
+ spawnSync("/bin/bash", ["-c", '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'], {
223
+ stdio: "inherit",
224
+ });
225
+ hasBrew = run("which brew") || run("eval $(/opt/homebrew/bin/brew shellenv) && which brew");
226
+ }
227
+ if (hasBrew) {
228
+ log("Python 3 not found. Installing via Homebrew...");
229
+ spawnSync("brew", ["install", "python3"], { stdio: "inherit" });
230
+ python = run("which python3");
231
+ }
232
+ } else if (platform === "linux") {
233
+ // Linux: try apt or yum
234
+ log("Python 3 not found. Attempting install...");
235
+ if (run("which apt-get")) {
236
+ spawnSync("sudo", ["apt-get", "install", "-y", "python3", "python3-pip", "python3-venv"], { stdio: "inherit" });
237
+ } else if (run("which yum")) {
238
+ spawnSync("sudo", ["yum", "install", "-y", "python3", "python3-pip"], { stdio: "inherit" });
239
+ }
230
240
  python = run("which python3");
231
241
  }
232
242
  if (!python) {
233
243
  log("Python 3 not found and couldn't install automatically.");
234
- log("Install it manually: brew install python3");
244
+ log(platform === "darwin" ? "Install it: brew install python3" : "Install it: sudo apt install python3");
235
245
  process.exit(1);
236
246
  }
237
247
  }
@@ -242,13 +252,20 @@ async function main() {
242
252
  let claudeInstalled = run("which claude");
243
253
  if (!claudeInstalled) {
244
254
  log("Claude Code not found. Installing...");
245
- const npmInstall = spawnSync("npm", ["install", "-g", "@anthropic-ai/claude-code"], {
246
- stdio: "inherit",
247
- });
248
- claudeInstalled = run("which claude");
255
+ // Try npx first (no sudo needed), then npm -g as fallback
256
+ spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], { stdio: "pipe", timeout: 60000 });
257
+ claudeInstalled = run("which claude") || run("npx -y @anthropic-ai/claude-code --version");
258
+ if (!claudeInstalled) {
259
+ // Fallback: npm -g (may need sudo on Linux)
260
+ const npmCmd = platform === "linux" ? "sudo" : "npm";
261
+ const npmArgs = platform === "linux" ? ["npm", "install", "-g", "@anthropic-ai/claude-code"] : ["install", "-g", "@anthropic-ai/claude-code"];
262
+ spawnSync(npmCmd, npmArgs, { stdio: "inherit" });
263
+ claudeInstalled = run("which claude");
264
+ }
249
265
  if (!claudeInstalled) {
250
266
  log("Could not install Claude Code automatically.");
251
267
  log("Install it manually: npm install -g @anthropic-ai/claude-code");
268
+ log("(On Linux you may need: sudo npm install -g @anthropic-ai/claude-code)");
252
269
  process.exit(1);
253
270
  }
254
271
  log("Claude Code installed successfully.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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": {
@@ -4,7 +4,7 @@ from db import create_agent, get_agent, list_agents, update_agent, delete_agent
4
4
  def handle_agent_get(id: str) -> str:
5
5
  """Get an agent's full profile by ID."""
6
6
  a = get_agent(id)
7
- if not a: return f"Agente '{id}' no encontrado."
7
+ if not a: return f"Agent '{id}' not found."
8
8
  lines = [f"AGENTE: {a['name']} ({a['id']})", f" Especialización: {a['specialization']}", f" Modelo: {a['model']}"]
9
9
  if a["tools"]: lines.append(f" Tools: {a['tools']}")
10
10
  if a["context_files"]: lines.append(f" Contexto: {a['context_files']}")
@@ -15,7 +15,7 @@ def handle_agent_create(id: str, name: str, specialization: str, model: str = "s
15
15
  tools: str = "", context_files: str = "", rules: str = "") -> str:
16
16
  """Register a new agent in the registry."""
17
17
  create_agent(id, name, specialization, model, tools, context_files, rules)
18
- return f"Agente '{id}' ({name}) registrado. Modelo: {model}"
18
+ return f"Agent '{id}' ({name}) registered. Model: {model}"
19
19
 
20
20
  def handle_agent_update(id: str, name: str = "", specialization: str = "", model: str = "",
21
21
  tools: str = "", context_files: str = "", rules: str = "") -> str:
@@ -26,12 +26,12 @@ def handle_agent_update(id: str, name: str = "", specialization: str = "", model
26
26
  if v: kwargs[k] = v
27
27
  if not kwargs: return "Nada que actualizar."
28
28
  update_agent(id, **kwargs)
29
- return f"Agente '{id}' actualizado."
29
+ return f"Agent '{id}' updated."
30
30
 
31
31
  def handle_agent_list() -> str:
32
32
  """List all registered agents."""
33
33
  agents = list_agents()
34
- if not agents: return "Sin agentes registrados."
34
+ if not agents: return "No agents registered."
35
35
  lines = ["AGENTES REGISTRADOS:"]
36
36
  for a in agents:
37
37
  lines.append(f" {a['id']} — {a['name']} ({a['model']}) — {a['specialization'][:60]}")
@@ -40,8 +40,8 @@ def handle_agent_list() -> str:
40
40
  def handle_agent_delete(id: str) -> str:
41
41
  """Remove an agent from the registry."""
42
42
  if not delete_agent(id):
43
- return f"ERROR: Agente '{id}' no encontrado."
44
- return f"Agente '{id}' eliminado."
43
+ return f"ERROR: Agent '{id}' not found."
44
+ return f"Agent '{id}' deleted."
45
45
 
46
46
  TOOLS = [
47
47
  (handle_agent_get, "nexo_agent_get", "Get an agent's full profile"),
@@ -56,7 +56,7 @@ def handle_backup_restore(filename: str) -> str:
56
56
  """
57
57
  src = os.path.join(BACKUP_DIR, filename)
58
58
  if not os.path.isfile(src):
59
- return f"Backup no encontrado: {filename}"
59
+ return f"Backup not found: {filename}"
60
60
 
61
61
  # Create safety backup first
62
62
  safety = os.path.join(BACKUP_DIR, f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db")
@@ -101,7 +101,7 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
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} en {days} días."
104
+ return f"Sin decisiones encontradas para {scope} in {days} days."
105
105
 
106
106
  lines = [f"DECISIONES ({len(results)}):"]
107
107
  for d in results:
@@ -312,7 +312,7 @@ def handle_change_search(query: str = '', files: str = '', days: int = 30) -> st
312
312
  results = search_changes(query, files, days)
313
313
  if not results:
314
314
  scope = f"'{query}'" if query else files or 'todos'
315
- return f"Sin cambios encontrados para {scope} en {days} días."
315
+ return f"No changes found for {scope} in {days} days."
316
316
 
317
317
  lines = [f"CAMBIOS ({len(results)}):"]
318
318
  for c in results:
@@ -618,17 +618,17 @@ Reporta cuantos registros eliminaste.""")
618
618
 
619
619
  tasks_str = "\n\n".join(tasks)
620
620
 
621
- return f"""Eres NEXO Sleep System. Tu trabajo es PODAR la memoria.
622
- NO eres interactivo. NO esperas input. Ejecuta las siguientes tareas y sal.
623
-
624
- REGLAS ABSOLUTAS:
625
- - NUNCA borres credenciales, tokens, IDs de cuentas, API endpoints, claves, secrets.
626
- - NUNCA borres reglas operativas marcadas como "CRITICO" o "MAXIMA PRIORIDAD".
627
- - NUNCA borres informacion sobre infraestructura (servidores, repos, deploys).
628
- - SI puedes fusionar secciones redundantes.
629
- - SI puedes eliminar informacion tecnica obsoleta (arreglada hace >30 dias y nunca referenciada despues).
630
- - SI puedes comprimir parrafos largos en bullets concisos.
631
- - Cada linea que elimines debe tener una razon clara. En caso de duda, NO borres.
621
+ return f"""You are NEXO Sleep System. Your job is to PRUNE memory.
622
+ You are NOT interactive. Do NOT wait for input. Execute the following tasks and exit.
623
+
624
+ ABSOLUTE RULES:
625
+ - NEVER delete credentials, tokens, account IDs, API endpoints, keys, secrets.
626
+ - NEVER delete operational rules marked as "CRITICAL" or "HIGHEST PRIORITY".
627
+ - NEVER delete information about infrastructure (servers, repos, deploys).
628
+ - You CAN merge redundant sections.
629
+ - You CAN remove obsolete technical information (fixed >30 days ago and never referenced since).
630
+ - You CAN compress long paragraphs into concise bullets.
631
+ - Every line you remove must have a clear reason. When in doubt, do NOT delete.
632
632
 
633
633
  {tasks_str}
634
634
 
@@ -24,7 +24,7 @@ def handle_learning_add(category: str, title: str, content: str, reasoning: str
24
24
  """
25
25
  if category not in VALID_CATEGORIES:
26
26
  valid = ", ".join(sorted(VALID_CATEGORIES))
27
- return f"ERROR: Categoría '{category}' inválida. Válidas: {valid}"
27
+ return f"ERROR: Category '{category}' invalid. Valid: {valid}"
28
28
  result = create_learning(
29
29
  category, title, content, reasoning=reasoning
30
30
  )
@@ -142,7 +142,7 @@ def handle_learning_update(id: int, title: str = '', content: str = '', category
142
142
  if category:
143
143
  if category not in VALID_CATEGORIES:
144
144
  valid = ", ".join(sorted(VALID_CATEGORIES))
145
- return f"ERROR: Categoría '{category}' inválida. Válidas: {valid}"
145
+ return f"ERROR: Category '{category}' invalid. Valid: {valid}"
146
146
  kwargs["category"] = category
147
147
  if reasoning:
148
148
  kwargs["reasoning"] = reasoning
@@ -183,7 +183,7 @@ def handle_learning_delete(id: int) -> str:
183
183
  """Delete a learning entry by ID."""
184
184
  deleted = delete_learning(id)
185
185
  if not deleted:
186
- return f"ERROR: Learning #{id} no encontrado."
186
+ return f"ERROR: Learning #{id} not found."
187
187
  return f"Learning #{id} eliminado."
188
188
 
189
189
 
@@ -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: El ID del recordatorio debe empezar por 'R' (recibido: '{id}')."
17
+ return f"ERROR: Reminder ID must start with 'R' (received: '{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", "desconocido") if isinstance(result, dict) else "desconocido"
21
+ error_msg = result.get("error", "unknown") if isinstance(result, dict) else "unknown"
22
22
  return f"ERROR: {error_msg}"
23
23
 
24
- fecha_str = date if date else 'sin fecha'
25
- return f"Recordatorio {id} creado. Fecha: {fecha_str}. Categoría: {category}."
24
+ date_str = date if date else 'no date'
25
+ return f"Reminder {id} created. Date: {date_str}. Category: {category}."
26
26
 
27
27
 
28
28
  def handle_reminder_update(id: str, description: str = '', date: str = '', status: str = '', category: str = '') -> str:
@@ -38,32 +38,32 @@ def handle_reminder_update(id: str, description: str = '', date: str = '', statu
38
38
  fields['category'] = category
39
39
 
40
40
  if not fields:
41
- return f"ERROR: No se especificó ningún campo a actualizar para {id}."
41
+ return f"ERROR: No fields specified to update for {id}."
42
42
 
43
43
  result = update_reminder(id=id, **fields)
44
44
  if not result:
45
- return f"ERROR: Recordatorio {id} no encontrado."
45
+ return f"ERROR: Reminder {id} not found."
46
46
 
47
47
  changed = ', '.join(fields.keys())
48
- return f"Recordatorio {id} actualizado: {changed}."
48
+ return f"Reminder {id} updated: {changed}."
49
49
 
50
50
 
51
51
  def handle_reminder_complete(id: str) -> str:
52
52
  """Mark a reminder as completed."""
53
53
  result = complete_reminder(id=id)
54
54
  if not result or "error" in result:
55
- return f"ERROR: Recordatorio {id} no encontrado."
55
+ return f"ERROR: Reminder {id} not found."
56
56
 
57
- return f"Recordatorio {id} marcado COMPLETADO."
57
+ return f"Reminder {id} marked COMPLETED."
58
58
 
59
59
 
60
60
  def handle_reminder_delete(id: str) -> str:
61
61
  """Delete a reminder permanently."""
62
62
  result = delete_reminder(id=id)
63
63
  if not result:
64
- return f"ERROR: Recordatorio {id} no encontrado."
64
+ return f"ERROR: Reminder {id} not found."
65
65
 
66
- return f"Recordatorio {id} eliminado."
66
+ return f"Reminder {id} deleted."
67
67
 
68
68
 
69
69
  # ── Followups ──────────────────────────────────────────────────────────────────
@@ -81,16 +81,16 @@ def handle_followup_create(id: str, description: str, date: str = '', verificati
81
81
  When completed, auto-creates the next occurrence.
82
82
  """
83
83
  if not id.startswith('NF'):
84
- return f"ERROR: El ID del followup debe empezar por 'NF' (recibido: '{id}')."
84
+ return f"ERROR: Followup ID must start with 'NF' (received: '{id}')."
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", "desconocido") if isinstance(result, dict) else "desconocido"
88
+ error_msg = result.get("error", "unknown") if isinstance(result, dict) else "unknown"
89
89
  return f"ERROR: {error_msg}"
90
90
 
91
- fecha_str = date if date else 'sin fecha'
92
- rec_str = f" Recurrencia: {recurrence}." if recurrence else ""
93
- return f"Followup {id} creado. Fecha: {fecha_str}.{rec_str}"
91
+ date_str = date if date else 'no date'
92
+ rec_str = f" Recurrence: {recurrence}." if recurrence else ""
93
+ return f"Followup {id} created. Date: {date_str}.{rec_str}"
94
94
 
95
95
 
96
96
  def handle_followup_update(id: str, description: str = '', date: str = '', verification: str = '', status: str = '') -> str:
@@ -106,14 +106,14 @@ def handle_followup_update(id: str, description: str = '', date: str = '', verif
106
106
  fields['status'] = status
107
107
 
108
108
  if not fields:
109
- return f"ERROR: No se especificó ningún campo a actualizar para {id}."
109
+ return f"ERROR: No fields specified to update for {id}."
110
110
 
111
111
  result = update_followup(id=id, **fields)
112
112
  if not result:
113
- return f"ERROR: Followup {id} no encontrado."
113
+ return f"ERROR: Followup {id} not found."
114
114
 
115
115
  changed = ', '.join(fields.keys())
116
- return f"Followup {id} actualizado: {changed}."
116
+ return f"Followup {id} updated: {changed}."
117
117
 
118
118
 
119
119
  def handle_followup_complete(id: str, result: str = '') -> str:
@@ -128,22 +128,22 @@ def handle_followup_complete(id: str, result: str = '') -> str:
128
128
 
129
129
  db_result = complete_followup(id=id, result=result)
130
130
  if not db_result or "error" in db_result:
131
- return f"ERROR: Followup {id} no encontrado."
131
+ return f"ERROR: Followup {id} not found."
132
132
 
133
133
  # Auto-link: find decisions whose context_ref matches this followup ID
134
- msg = f"Followup {id} marcado COMPLETADO."
134
+ msg = f"Followup {id} marked COMPLETED."
135
135
  if has_recurrence:
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" ♻️ Siguiente auto-creado para {new_row['date']}."
139
+ msg += f" Next auto-created for {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)
146
- msg += f" Decision(s) {dec_ids} actualizada(s) con outcome automático."
146
+ msg += f" Decision(s) {dec_ids} updated with automatic outcome."
147
147
 
148
148
  return msg
149
149
 
@@ -152,6 +152,6 @@ def handle_followup_delete(id: str) -> str:
152
152
  """Delete a followup permanently."""
153
153
  result = delete_followup(id=id)
154
154
  if not result:
155
- return f"ERROR: Followup {id} no encontrado."
155
+ return f"ERROR: Followup {id} not found."
156
156
 
157
- return f"Followup {id} eliminado."
157
+ return f"Followup {id} deleted."