nexo-brain 6.1.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -1
- package/package.json +2 -2
- package/src/auto_update.py +44 -0
- package/src/classifier_local.py +176 -0
- package/src/cli.py +15 -0
- package/src/cli_email.py +492 -0
- package/src/cognitive/_core.py +36 -0
- package/src/cognitive/_trust.py +95 -10
- package/src/db/_core.py +5 -0
- package/src/db/_email_accounts.py +170 -0
- package/src/db/_schema.py +88 -0
- package/src/email_config.py +143 -0
- package/src/enforcement_classifier.py +31 -6
- package/src/enforcement_engine.py +159 -0
- package/src/fase_f_loops.py +194 -0
- package/src/hook_guardrails.py +14 -0
- package/src/hooks/auto_capture.py +67 -0
- package/src/nexo_migrate.py +158 -0
- package/src/plugin_loader.py +86 -0
- package/src/plugins/cognitive_memory.py +3 -0
- package/src/presets/entities_local.sample.json +30 -0
- package/src/presets/entities_universal.json +22 -58
- package/src/presets/guardian_default.json +2 -1
- package/src/r23b_deploy_vhost.py +2 -2
- package/src/r34_identity_coherence.py +132 -0
- package/src/r_catalog.py +72 -0
- package/src/scripts/nexo-email-migrate-config.py +145 -0
- package/src/scripts/phase_guardian_analysis.py +114 -0
- package/src/server.py +31 -1
- package/src/system_catalog.py +54 -0
- package/src/t4_llm_gate.py +174 -0
- package/src/tools_email_guard.py +88 -0
- package/src/tools_guardian.py +183 -0
- package/templates/CLAUDE.md.template +9 -0
- package/templates/CODEX.AGENTS.md.template +7 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `6.
|
|
21
|
+
Version `6.4.0` is the current packaged-runtime line — Plan Consolidado fase F1: multi-tenant email accounts (`email_accounts` table, `nexo email setup` interactive wizard, `nexo email add --password-stdin --json` for the Desktop bridge, idempotent migrator from legacy `~/.nexo/nexo-email/config.json`). The matching NEXO Desktop release (v0.19.0) ships the Email + Automations Settings panels so non-technical operators never have to touch a config file. See [CHANGELOG](CHANGELOG.md) for the full diff.
|
|
22
|
+
|
|
23
|
+
Previously in `6.3.1`: privacy hotfix over v6.3.0. The nightly auditor caught that `src/presets/entities_universal.json` in v6.3.0 shipped operator-specific `vhost_mapping` entries (private IPs, hostnames, tenant names). v6.3.1 pulls those out into `src/presets/entities_local.sample.json` (template) + `.gitignore`'d `~/.nexo/brain/presets/entities_local.json` (operator copy), and the installer drops the sample at `nexo init`. No behaviour change on the Guardian side.
|
|
24
|
+
|
|
25
|
+
Previously in `6.3.0` — Plan Consolidado wave 2, coordinated with NEXO Desktop v0.18.0. Closes the remaining Guardian roadmap items that do not require an invasive structure migration: extended `cognitive_sentiment` shape (is_correction/valence/intent), extended `entities` schema, 21 labelled rule fixtures with R13 spike gates, Fase F telemetry loops + Deep Sleep phase, pinned local zero-shot classifier skeleton (mDeBERTa), hook respects `NEXO_MIGRATING=1`, `origin` column on `personal_scripts`, and the T4 LLM gate wrapping R15/R23e/R23f/R23h (byte-parity Py ↔ JS). Two pre-release auditors flagged a CRITICAL in the first JS wire (method-name + async mismatch) and a HIGH (classifier bool conflated "no" with "unparseable"); both corrected with regression tests before merge.
|
|
26
|
+
|
|
27
|
+
Previously in `6.1.1`: small fix to `nexo --help` so the `Latest: vX` line reliably appears when NEXO Desktop invokes the CLI via subprocess — unblocks the Desktop Brain auto-update banner that previously couldn't parse the version delta. No behaviour change for interactive terminal users; the 6-hour registry cache still rate-limits network calls. Bundles all v6.1.0 Protocol Enforcer Fase 2 + multi-claude-sid hotfix content.
|
|
22
28
|
|
|
23
29
|
Previously in `6.0.2`: adds the reserved caller prefix `personal/*` so scripts living in `~/.nexo/scripts/` can invoke the automation backend with their own caller id without editing `src/resonance_map.py`. New kwarg `tier` (`"maximo"` / `"alto"` / `"medio"` / `"bajo"`) on `run_automation_prompt`, `run_automation_interactive`, `nexo_helper.run_automation_text`, `nexo_helper.run_automation_json`, and `nexo-agent-run.py --tier`. Precedence for `personal/*` callers: explicit `tier=` → explicit `reasoning_effort=` → `calibration.preferences.default_resonance` → `DEFAULT_RESONANCE` (`alto`). Registered callers keep their behaviour unchanged. New guide: [`docs/personal-scripts-guide.md`](docs/personal-scripts-guide.md).
|
|
24
30
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
|
-
"description": "NEXO Brain
|
|
5
|
+
"description": "NEXO Brain \u2014 Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
7
7
|
"bin": {
|
|
8
8
|
"nexo-brain": "./bin/nexo-brain.js",
|
package/src/auto_update.py
CHANGED
|
@@ -1519,12 +1519,56 @@ def _run_db_migrations() -> bool:
|
|
|
1519
1519
|
applied = run_migrations(conn)
|
|
1520
1520
|
if applied > 0:
|
|
1521
1521
|
_log(f"Applied {applied} DB migration(s)")
|
|
1522
|
+
# Plan Consolidado F1 — one-shot legacy email config migration.
|
|
1523
|
+
# After m46 adds the table, operators installed pre-v6.4.0 still
|
|
1524
|
+
# keep their data inside ~/.nexo/nexo-email/config.json. If the
|
|
1525
|
+
# table is empty and the JSON exists, port the primary account
|
|
1526
|
+
# over automatically so scripts pick up the new source on the
|
|
1527
|
+
# next cron without the operator running anything manually.
|
|
1528
|
+
_maybe_migrate_legacy_email_config()
|
|
1522
1529
|
return True
|
|
1523
1530
|
except Exception as e:
|
|
1524
1531
|
_log(f"DB migration error: {e}")
|
|
1525
1532
|
return False
|
|
1526
1533
|
|
|
1527
1534
|
|
|
1535
|
+
def _maybe_migrate_legacy_email_config() -> None:
|
|
1536
|
+
"""F1 auto-migrator — idempotent. Runs the helper script the first
|
|
1537
|
+
time after v6.4.0 lands on an existing runtime."""
|
|
1538
|
+
try:
|
|
1539
|
+
from db._email_accounts import get_email_account
|
|
1540
|
+
except Exception:
|
|
1541
|
+
return # m46 not applied yet (older runtime), nothing to do
|
|
1542
|
+
try:
|
|
1543
|
+
legacy = NEXO_HOME / "nexo-email" / "config.json"
|
|
1544
|
+
if not legacy.exists():
|
|
1545
|
+
return
|
|
1546
|
+
if get_email_account("primary"):
|
|
1547
|
+
return # already migrated
|
|
1548
|
+
import subprocess as _sp
|
|
1549
|
+
script = Path(__file__).resolve().parent / "scripts" / "nexo-email-migrate-config.py"
|
|
1550
|
+
if not script.exists():
|
|
1551
|
+
return
|
|
1552
|
+
_log("F1: migrating legacy email config.json → email_accounts table")
|
|
1553
|
+
env = {**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parent)}
|
|
1554
|
+
r = _sp.run(
|
|
1555
|
+
[sys.executable, str(script)],
|
|
1556
|
+
env=env,
|
|
1557
|
+
capture_output=True,
|
|
1558
|
+
text=True,
|
|
1559
|
+
timeout=30,
|
|
1560
|
+
)
|
|
1561
|
+
if r.returncode == 0:
|
|
1562
|
+
line = (r.stdout.strip().splitlines() or ["ok"])[-1]
|
|
1563
|
+
_log(f"F1 email migration: {line}")
|
|
1564
|
+
else:
|
|
1565
|
+
_log(f"F1 email migration FAILED (rc={r.returncode}): {r.stderr.strip()[:200]}")
|
|
1566
|
+
except _sp.TimeoutExpired:
|
|
1567
|
+
_log("F1 email migration timed out after 30s")
|
|
1568
|
+
except Exception as exc:
|
|
1569
|
+
_log(f"F1 email migration skipped: {exc}")
|
|
1570
|
+
|
|
1571
|
+
|
|
1528
1572
|
# ── npm version check (notify only) ─────────────────────────────────
|
|
1529
1573
|
|
|
1530
1574
|
def _check_npm_version() -> str | None:
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Plan Consolidado 0.21 — Local zero-shot multilingual classifier.
|
|
2
|
+
|
|
3
|
+
Skeleton + pinned HuggingFace coordinates. The heavy load
|
|
4
|
+
(`transformers`, ~500 MB model download) is lazy so the rest of the
|
|
5
|
+
runtime does not pay the cost on every import.
|
|
6
|
+
|
|
7
|
+
Contract:
|
|
8
|
+
|
|
9
|
+
clf = LocalZeroShotClassifier()
|
|
10
|
+
result = clf.classify(
|
|
11
|
+
"lo hemos dejado, ya estaría",
|
|
12
|
+
labels=("done_claim", "status_update", "question", "noise"),
|
|
13
|
+
)
|
|
14
|
+
result == {"label": "done_claim", "confidence": 0.87, "scores": {...}}
|
|
15
|
+
|
|
16
|
+
When transformers is not installed or the download fails (offline),
|
|
17
|
+
`classify` returns `None` and `classify_fail_closed` returns a
|
|
18
|
+
conservative fallback label so rules degrade gracefully (item 0.20).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import threading
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Iterable
|
|
27
|
+
|
|
28
|
+
_logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Keep in lockstep with docs/classifier-model-notes.md.
|
|
32
|
+
# Plan 0.21 wave-2 update: the original pin
|
|
33
|
+
# (MoritzLaurer/mDeBERTa-v3-base-mnli-xnli @ a1a5a76) refused to load
|
|
34
|
+
# under transformers 5.x with a missing `model_type` error. Switched
|
|
35
|
+
# to the multilingual-2mil7 sibling which is the same DeBERTa-v2
|
|
36
|
+
# architecture, multilingual, and loads cleanly. Revision pinned to
|
|
37
|
+
# the last HF upstream commit verified in smoke.
|
|
38
|
+
MODEL_ID = "MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7"
|
|
39
|
+
MODEL_REVISION = "b5113eb38ab63efdd7f280f8c144ea8b13f978ce"
|
|
40
|
+
DEFAULT_CONFIDENCE_FLOOR = 0.6
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ClassificationResult:
|
|
45
|
+
label: str
|
|
46
|
+
confidence: float
|
|
47
|
+
scores: dict[str, float]
|
|
48
|
+
latency_ms: float
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LocalZeroShotClassifier:
|
|
52
|
+
"""Lazy wrapper around transformers' zero-shot-classification pipeline.
|
|
53
|
+
|
|
54
|
+
Thread-safe lazy load; failures degrade to `classify(...) = None` so
|
|
55
|
+
the Guardian can decide whether to invoke the LLM fallback
|
|
56
|
+
(`call_model_raw`) or a conservative regex path.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
model_id: str = MODEL_ID,
|
|
63
|
+
revision: str = MODEL_REVISION,
|
|
64
|
+
confidence_floor: float = DEFAULT_CONFIDENCE_FLOOR,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.model_id = model_id
|
|
67
|
+
self.revision = revision
|
|
68
|
+
self.confidence_floor = confidence_floor
|
|
69
|
+
self._pipe = None
|
|
70
|
+
self._load_failed = False
|
|
71
|
+
self._lock = threading.Lock()
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Lazy load
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
def _ensure_loaded(self) -> bool:
|
|
77
|
+
if self._pipe is not None:
|
|
78
|
+
return True
|
|
79
|
+
if self._load_failed:
|
|
80
|
+
return False
|
|
81
|
+
with self._lock:
|
|
82
|
+
if self._pipe is not None:
|
|
83
|
+
return True
|
|
84
|
+
if self._load_failed:
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
from transformers import pipeline # type: ignore
|
|
88
|
+
except Exception as exc: # pragma: no cover — no HF on CI
|
|
89
|
+
_logger.warning(
|
|
90
|
+
"classifier_local disabled: transformers unavailable (%s)",
|
|
91
|
+
exc,
|
|
92
|
+
)
|
|
93
|
+
self._load_failed = True
|
|
94
|
+
return False
|
|
95
|
+
try:
|
|
96
|
+
self._pipe = pipeline(
|
|
97
|
+
"zero-shot-classification",
|
|
98
|
+
model=self.model_id,
|
|
99
|
+
revision=self.revision,
|
|
100
|
+
device=-1, # CPU-only
|
|
101
|
+
)
|
|
102
|
+
return True
|
|
103
|
+
except Exception as exc: # pragma: no cover — network / disk
|
|
104
|
+
_logger.warning(
|
|
105
|
+
"classifier_local pipeline failed to initialise: %s", exc
|
|
106
|
+
)
|
|
107
|
+
self._load_failed = True
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Public API
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
def is_available(self) -> bool:
|
|
114
|
+
return self._ensure_loaded()
|
|
115
|
+
|
|
116
|
+
def classify(
|
|
117
|
+
self,
|
|
118
|
+
text: str,
|
|
119
|
+
labels: Iterable[str],
|
|
120
|
+
*,
|
|
121
|
+
multi_label: bool = False,
|
|
122
|
+
) -> ClassificationResult | None:
|
|
123
|
+
"""Return best label + confidence or None if the local pipeline
|
|
124
|
+
is unavailable."""
|
|
125
|
+
if not text or not labels:
|
|
126
|
+
return None
|
|
127
|
+
if not self._ensure_loaded():
|
|
128
|
+
return None
|
|
129
|
+
import time
|
|
130
|
+
t0 = time.time()
|
|
131
|
+
try:
|
|
132
|
+
raw = self._pipe( # type: ignore[operator]
|
|
133
|
+
text,
|
|
134
|
+
candidate_labels=list(labels),
|
|
135
|
+
multi_label=multi_label,
|
|
136
|
+
)
|
|
137
|
+
except Exception as exc: # pragma: no cover
|
|
138
|
+
_logger.warning("classifier_local inference failed: %s", exc)
|
|
139
|
+
return None
|
|
140
|
+
latency_ms = (time.time() - t0) * 1000.0
|
|
141
|
+
scores = dict(zip(raw["labels"], raw["scores"]))
|
|
142
|
+
top_label = raw["labels"][0]
|
|
143
|
+
return ClassificationResult(
|
|
144
|
+
label=top_label,
|
|
145
|
+
confidence=float(raw["scores"][0]),
|
|
146
|
+
scores=scores,
|
|
147
|
+
latency_ms=latency_ms,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def classify_fail_closed(
|
|
151
|
+
self,
|
|
152
|
+
text: str,
|
|
153
|
+
labels: Iterable[str],
|
|
154
|
+
fallback_label: str,
|
|
155
|
+
) -> ClassificationResult:
|
|
156
|
+
"""Never returns None — falls back to `fallback_label` with
|
|
157
|
+
confidence 0 so the Guardian can still decide without crashing.
|
|
158
|
+
"""
|
|
159
|
+
got = self.classify(text, labels)
|
|
160
|
+
if got is not None and got.confidence >= self.confidence_floor:
|
|
161
|
+
return got
|
|
162
|
+
return ClassificationResult(
|
|
163
|
+
label=fallback_label,
|
|
164
|
+
confidence=0.0,
|
|
165
|
+
scores={label: 0.0 for label in labels},
|
|
166
|
+
latency_ms=0.0,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
__all__ = [
|
|
171
|
+
"LocalZeroShotClassifier",
|
|
172
|
+
"ClassificationResult",
|
|
173
|
+
"MODEL_ID",
|
|
174
|
+
"MODEL_REVISION",
|
|
175
|
+
"DEFAULT_CONFIDENCE_FLOOR",
|
|
176
|
+
]
|
package/src/cli.py
CHANGED
|
@@ -2046,6 +2046,13 @@ def main():
|
|
|
2046
2046
|
parser.add_argument("-v", "--version", action="store_true", help="Show version")
|
|
2047
2047
|
sub = parser.add_subparsers(dest="command")
|
|
2048
2048
|
|
|
2049
|
+
# -- email (Plan F1 — interactive wizard for email accounts) --
|
|
2050
|
+
try:
|
|
2051
|
+
from cli_email import register_email_parser
|
|
2052
|
+
register_email_parser(sub)
|
|
2053
|
+
except Exception as _exc_email: # pragma: no cover
|
|
2054
|
+
pass
|
|
2055
|
+
|
|
2049
2056
|
# -- chat --
|
|
2050
2057
|
chat_parser = sub.add_parser("chat", help="Launch a NEXO terminal client")
|
|
2051
2058
|
chat_parser.add_argument("path", nargs="?", default=".", help="Working directory (default: current directory)")
|
|
@@ -2354,6 +2361,14 @@ def main():
|
|
|
2354
2361
|
print(f"nexo v{_get_version()}")
|
|
2355
2362
|
return 0
|
|
2356
2363
|
|
|
2364
|
+
if args.command == "email":
|
|
2365
|
+
# Plan F1 — setup / list / test / remove cuentas email.
|
|
2366
|
+
fn = getattr(args, "func", None)
|
|
2367
|
+
if fn is None:
|
|
2368
|
+
print("usage: nexo email {setup,list,test,remove}")
|
|
2369
|
+
return 1
|
|
2370
|
+
return int(fn(args) or 0)
|
|
2371
|
+
|
|
2357
2372
|
if args.command == "scripts":
|
|
2358
2373
|
if args.scripts_command == "list":
|
|
2359
2374
|
return _scripts_list(args)
|