nexo-brain 7.9.8 → 7.9.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/nexo-brain.js +145 -8
- package/package.json +1 -1
- package/src/doctor/planes.py +9 -5
- package/src/plugins/doctor.py +0 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.10",
|
|
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,7 @@
|
|
|
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 `7.9.
|
|
21
|
+
Version `7.9.10` is the current packaged-runtime line. Hotfix release over `7.9.9`: `nexo_doctor` now defaults blank calls to `runtime_personal` instead of failing on missing `plane`, and non-interactive installer/update paths preserve existing identity defaults from runtime profile/calibration metadata instead of blindly rewriting operators to generic onboarding defaults. Desktop remains at v0.28.11 because the extra fixes are Brain-only.
|
|
22
22
|
|
|
23
23
|
Previously in `7.9.5`: patch release that fixes canonical diary confirmation for Desktop: Brain resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.6 shutdown/archive/delete/app-exit checks.
|
|
24
24
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -252,6 +252,24 @@ function readRuntimeCalibration(nexoHome = NEXO_HOME) {
|
|
|
252
252
|
return { path: null, payload: null };
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
function profilePathCandidates(nexoHome = NEXO_HOME) {
|
|
256
|
+
return [
|
|
257
|
+
path.join(nexoHome, "personal", "brain", "profile.json"),
|
|
258
|
+
path.join(nexoHome, "brain", "profile.json"),
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readRuntimeProfile(nexoHome = NEXO_HOME) {
|
|
263
|
+
for (const filePath of profilePathCandidates(nexoHome)) {
|
|
264
|
+
if (!fs.existsSync(filePath)) continue;
|
|
265
|
+
const payload = readJsonFile(filePath);
|
|
266
|
+
if (payload && typeof payload === "object") {
|
|
267
|
+
return { path: filePath, payload };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { path: null, payload: null };
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
function nonEmptyString(value) {
|
|
256
274
|
return typeof value === "string" && value.trim().length > 0;
|
|
257
275
|
}
|
|
@@ -288,6 +306,55 @@ function hasPartialPlaceholderCalibration(calibration) {
|
|
|
288
306
|
return isPlaceholderUserName(name) || !nonEmptyString(language);
|
|
289
307
|
}
|
|
290
308
|
|
|
309
|
+
function firstMeaningfulString(...values) {
|
|
310
|
+
for (const value of values) {
|
|
311
|
+
if (typeof value !== "string") continue;
|
|
312
|
+
const clean = value.trim();
|
|
313
|
+
if (clean) return clean;
|
|
314
|
+
}
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function normalizeLanguageCode(value) {
|
|
319
|
+
const clean = String(value || "").trim().toLowerCase().replace("_", "-");
|
|
320
|
+
if (!clean) return "";
|
|
321
|
+
return clean.split("-")[0];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function resolveExistingIdentityDefaults(nexoHome = NEXO_HOME) {
|
|
325
|
+
const calibration = readRuntimeCalibration(nexoHome).payload || {};
|
|
326
|
+
const user = calibration.user && typeof calibration.user === "object" ? calibration.user : {};
|
|
327
|
+
const profile = readRuntimeProfile(nexoHome).payload || {};
|
|
328
|
+
const version = readJsonFile(path.join(nexoHome, "version.json")) || {};
|
|
329
|
+
|
|
330
|
+
const userName = firstMeaningfulString(
|
|
331
|
+
user.name,
|
|
332
|
+
calibration.user_name,
|
|
333
|
+
profile.user_name,
|
|
334
|
+
version.user_name,
|
|
335
|
+
);
|
|
336
|
+
const language = normalizeLanguageCode(firstMeaningfulString(
|
|
337
|
+
user.language,
|
|
338
|
+
calibration.language,
|
|
339
|
+
profile.language,
|
|
340
|
+
version.language,
|
|
341
|
+
));
|
|
342
|
+
const operatorName = firstMeaningfulString(
|
|
343
|
+
user.assistant_name,
|
|
344
|
+
calibration.assistant_name,
|
|
345
|
+
calibration.operator_name,
|
|
346
|
+
profile.assistant_name,
|
|
347
|
+
profile.operator_name,
|
|
348
|
+
version.operator_name,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
userName: !isPlaceholderUserName(userName) ? userName : "",
|
|
353
|
+
language,
|
|
354
|
+
operatorName: !isReservedAssistantName(operatorName) ? operatorName : "",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
291
358
|
function ensureOnboardingCompletionMarker(nexoHome = NEXO_HOME) {
|
|
292
359
|
const record = readRuntimeCalibration(nexoHome);
|
|
293
360
|
if (!record.path || !isOnboardingComplete(record.payload)) {
|
|
@@ -684,6 +751,59 @@ function finalizeF06Layout(python, nexoHome = NEXO_HOME) {
|
|
|
684
751
|
}
|
|
685
752
|
}
|
|
686
753
|
|
|
754
|
+
function readRuntimeVersionFrom(basePath) {
|
|
755
|
+
if (!basePath) return "";
|
|
756
|
+
for (const candidate of [
|
|
757
|
+
path.join(basePath, "version.json"),
|
|
758
|
+
path.join(basePath, "package.json"),
|
|
759
|
+
]) {
|
|
760
|
+
try {
|
|
761
|
+
if (!fs.existsSync(candidate)) continue;
|
|
762
|
+
const payload = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
763
|
+
const version = String(payload.version || "").trim();
|
|
764
|
+
if (version) return version;
|
|
765
|
+
} catch (_) {}
|
|
766
|
+
}
|
|
767
|
+
return "";
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function readActiveRuntimeSnapshotVersion(nexoHome = NEXO_HOME) {
|
|
771
|
+
return readRuntimeVersionFrom(path.join(nexoHome, "core", "current"));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function activateVersionedRuntimeSnapshot(python, nexoHome = NEXO_HOME, version = "") {
|
|
775
|
+
try {
|
|
776
|
+
const srcDir = path.join(__dirname, "..", "src");
|
|
777
|
+
const inline = [
|
|
778
|
+
"import json, os, pathlib, sys",
|
|
779
|
+
`sys.path.insert(0, ${JSON.stringify(srcDir)})`,
|
|
780
|
+
"from runtime_versioning import activate_versioned_runtime_snapshot",
|
|
781
|
+
"home = pathlib.Path(os.environ['NEXO_HOME'])",
|
|
782
|
+
`result = activate_versioned_runtime_snapshot(source_root=home / 'core', version=${JSON.stringify(version)})`,
|
|
783
|
+
"print(json.dumps(result))",
|
|
784
|
+
].join("; ");
|
|
785
|
+
const result = spawnSync(python, ["-c", inline], {
|
|
786
|
+
cwd: nexoHome,
|
|
787
|
+
env: {
|
|
788
|
+
...process.env,
|
|
789
|
+
NEXO_HOME: nexoHome,
|
|
790
|
+
},
|
|
791
|
+
encoding: "utf8",
|
|
792
|
+
});
|
|
793
|
+
if (result.status !== 0) {
|
|
794
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
795
|
+
throw new Error(detail || "activation command failed");
|
|
796
|
+
}
|
|
797
|
+
const payload = JSON.parse(String(result.stdout || "{}").trim() || "{}");
|
|
798
|
+
if (!payload || payload.ok !== true) {
|
|
799
|
+
throw new Error(payload && payload.error ? payload.error : "activation returned not-ok");
|
|
800
|
+
}
|
|
801
|
+
return payload;
|
|
802
|
+
} catch (err) {
|
|
803
|
+
return { ok: false, error: String((err && err.message) || err) };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
687
807
|
function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
|
|
688
808
|
const staticFiles = [
|
|
689
809
|
"server.py",
|
|
@@ -2296,10 +2416,17 @@ async function runSetup() {
|
|
|
2296
2416
|
const currentPkg = readPackageJson();
|
|
2297
2417
|
const installedVersion = installed.version || "0.0.0";
|
|
2298
2418
|
const currentVersion = currentPkg.version;
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2419
|
+
const activeRuntimeVersion = readActiveRuntimeSnapshotVersion(NEXO_HOME);
|
|
2420
|
+
const needsRuntimeRepair = activeRuntimeVersion !== currentVersion;
|
|
2421
|
+
|
|
2422
|
+
if (installedVersion !== currentVersion || needsRuntimeRepair) {
|
|
2423
|
+
if (installedVersion !== currentVersion) {
|
|
2424
|
+
log(`Existing installation detected: v${installedVersion} → v${currentVersion}`);
|
|
2425
|
+
log("Running auto-migration...");
|
|
2426
|
+
} else {
|
|
2427
|
+
log(`Existing installation detected: metadata v${installedVersion}, runtime core/current v${activeRuntimeVersion || "missing"}`);
|
|
2428
|
+
log("Repairing active runtime snapshot...");
|
|
2429
|
+
}
|
|
2303
2430
|
|
|
2304
2431
|
// Recursive copy helper (skips __pycache__, .pyc, .db files)
|
|
2305
2432
|
const srcDir = path.join(__dirname, "..", "src");
|
|
@@ -2469,6 +2596,9 @@ async function runSetup() {
|
|
|
2469
2596
|
installed_at: installed.installed_at,
|
|
2470
2597
|
updated_at: new Date().toISOString(),
|
|
2471
2598
|
migrated_from: installedVersion,
|
|
2599
|
+
...(installedVersion === currentVersion && activeRuntimeVersion && activeRuntimeVersion !== currentVersion
|
|
2600
|
+
? { runtime_repaired_from: activeRuntimeVersion }
|
|
2601
|
+
: {}),
|
|
2472
2602
|
}, null, 2));
|
|
2473
2603
|
syncRuntimePackageMetadata(path.join(__dirname, ".."), NEXO_HOME);
|
|
2474
2604
|
log("Finalizing F0.6 runtime layout...");
|
|
@@ -2476,6 +2606,11 @@ async function runSetup() {
|
|
|
2476
2606
|
if (!migLayoutFinalize.ok) {
|
|
2477
2607
|
throw new Error(`F0.6 layout finalization failed: ${migLayoutFinalize.error}`);
|
|
2478
2608
|
}
|
|
2609
|
+
const migActivation = activateVersionedRuntimeSnapshot(migPython, NEXO_HOME, currentVersion);
|
|
2610
|
+
if (!migActivation.ok) {
|
|
2611
|
+
throw new Error(`Runtime activation failed: ${migActivation.error}`);
|
|
2612
|
+
}
|
|
2613
|
+
log(` Runtime activation: core/current -> versions/${currentVersion}`);
|
|
2479
2614
|
|
|
2480
2615
|
// Keep the rendered template in-memory for version tracking, but do
|
|
2481
2616
|
// not drop a loose reference file in NEXO_HOME root.
|
|
@@ -2898,9 +3033,11 @@ async function runSetup() {
|
|
|
2898
3033
|
},
|
|
2899
3034
|
};
|
|
2900
3035
|
|
|
3036
|
+
const existingIdentity = resolveExistingIdentityDefaults(NEXO_HOME);
|
|
3037
|
+
|
|
2901
3038
|
// Detect language from input or use default
|
|
2902
|
-
let lang = "en";
|
|
2903
|
-
let t = i18n.en;
|
|
3039
|
+
let lang = existingIdentity.language || "en";
|
|
3040
|
+
let t = i18n[lang] || i18n.en;
|
|
2904
3041
|
if (!useDefaults) {
|
|
2905
3042
|
const langInput = await ask(" What's your preferred language? / ¿En qué idioma prefieres hablar?\n > ");
|
|
2906
3043
|
const langLower = langInput.trim().toLowerCase();
|
|
@@ -2941,7 +3078,7 @@ async function runSetup() {
|
|
|
2941
3078
|
// Step 2: User's name (P2) — v6.0.0 empty input falls through to "Usuario"
|
|
2942
3079
|
// instead of keeping an empty string. The calibration file always ships
|
|
2943
3080
|
// with a concrete user.name so downstream tooling does not need guards.
|
|
2944
|
-
let userName = "Usuario";
|
|
3081
|
+
let userName = existingIdentity.userName || "Usuario";
|
|
2945
3082
|
if (!useDefaults) {
|
|
2946
3083
|
const nameInput = await ask(t.askUserName);
|
|
2947
3084
|
const trimmedName = nameInput.trim();
|
|
@@ -2953,7 +3090,7 @@ async function runSetup() {
|
|
|
2953
3090
|
}
|
|
2954
3091
|
|
|
2955
3092
|
// Step 3: Agent name (P3)
|
|
2956
|
-
let operatorName = DEFAULT_ASSISTANT_NAME;
|
|
3093
|
+
let operatorName = existingIdentity.operatorName || DEFAULT_ASSISTANT_NAME;
|
|
2957
3094
|
if (!useDefaults) {
|
|
2958
3095
|
while (true) {
|
|
2959
3096
|
const name = await ask(t.askAgentName);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.10",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — 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",
|
package/src/doctor/planes.py
CHANGED
|
@@ -28,6 +28,7 @@ VALID_DIAGNOSTIC_PLANES = {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
DOCTOR_COMPATIBLE_PLANES = {"runtime_personal", "installation_live", "database_real"}
|
|
31
|
+
DEFAULT_DOCTOR_PLANE = "runtime_personal"
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def normalize_diagnostic_plane(plane: str = "") -> str:
|
|
@@ -40,15 +41,19 @@ def diagnostic_plane_choices() -> list[str]:
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def diagnostic_plane_preflight(plane: str = "") -> tuple[str, DoctorCheck | None]:
|
|
43
|
-
|
|
44
|
+
raw_plane = str(plane or "").strip()
|
|
45
|
+
if not raw_plane:
|
|
46
|
+
return DEFAULT_DOCTOR_PLANE, None
|
|
47
|
+
|
|
48
|
+
clean_plane = normalize_diagnostic_plane(raw_plane)
|
|
44
49
|
if not clean_plane:
|
|
45
50
|
options = ", ".join(diagnostic_plane_choices())
|
|
46
51
|
return "", DoctorCheck(
|
|
47
|
-
id="orchestrator.
|
|
52
|
+
id="orchestrator.diagnostic_plane_invalid",
|
|
48
53
|
tier="orchestrator",
|
|
49
54
|
status="critical",
|
|
50
55
|
severity="error",
|
|
51
|
-
summary="
|
|
56
|
+
summary=f"Plano diagnóstico desconocido: {raw_plane}",
|
|
52
57
|
evidence=[
|
|
53
58
|
f"planes válidos: {options}",
|
|
54
59
|
"Usa `runtime_personal` para ~/.nexo y hábitos del runtime; `installation_live` para hooks/clientes/instalación; `database_real` para filas y schema reales.",
|
|
@@ -58,8 +63,7 @@ def diagnostic_plane_preflight(plane: str = "") -> tuple[str, DoctorCheck | None
|
|
|
58
63
|
"Si el problema pertenece a producto público o al co-operador, usa el surface correcto en vez de NEXO Doctor.",
|
|
59
64
|
],
|
|
60
65
|
escalation_prompt=(
|
|
61
|
-
"
|
|
62
|
-
"explícitamente si el problema está en producto público, runtime personal, instalación viva, BD real o co-operador."
|
|
66
|
+
"El plano elegido no existe. Repite el diagnóstico con un plano válido para evitar mezclar runtime, instalación, BD real o surfaces ajenas al doctor."
|
|
63
67
|
),
|
|
64
68
|
)
|
|
65
69
|
|
package/src/plugins/doctor.py
CHANGED
|
@@ -22,26 +22,11 @@ def handle_doctor(tier: str = "boot", fix: bool = False, output: str = "text", p
|
|
|
22
22
|
"""
|
|
23
23
|
from doctor.orchestrator import run_doctor
|
|
24
24
|
from doctor.formatters import format_report
|
|
25
|
-
from doctor.planes import diagnostic_plane_choices
|
|
26
25
|
|
|
27
26
|
if tier not in ("boot", "runtime", "deep", "all"):
|
|
28
27
|
return f"Invalid tier '{tier}'. Use: boot, runtime, deep, all"
|
|
29
28
|
if output not in ("text", "json"):
|
|
30
29
|
return f"Invalid output '{output}'. Use: text, json"
|
|
31
|
-
if not (plane or "").strip():
|
|
32
|
-
valid_planes = diagnostic_plane_choices()
|
|
33
|
-
if output == "json":
|
|
34
|
-
return (
|
|
35
|
-
"{"
|
|
36
|
-
f"\"ok\": false, \"error\": \"Missing required argument: plane\", "
|
|
37
|
-
"\"missing_argument\": \"plane\", "
|
|
38
|
-
f"\"valid_planes\": {valid_planes!r}"
|
|
39
|
-
"}"
|
|
40
|
-
).replace("'", '"')
|
|
41
|
-
return (
|
|
42
|
-
"Missing required argument: plane. "
|
|
43
|
-
f"Use one of: {', '.join(valid_planes)}."
|
|
44
|
-
)
|
|
45
30
|
|
|
46
31
|
report = run_doctor(tier=tier, fix=fix, plane=plane)
|
|
47
32
|
return format_report(report, fmt=output)
|