nexo-brain 7.9.9 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.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.9` is the current packaged-runtime line. Hotfix release over `7.9.8`: after the packaged updater fix, the installer now also repairs installs whose metadata already says the new version but whose active runtime still points to an older `~/.nexo/core/current -> versions/<old>`. Running `nexo-brain --yes` or the postinstall path now recopies the packaged runtime, re-activates `core/current`, and leaves installed users on the real new code instead of a stale snapshot. Coordinated Desktop v0.28.11 bundles the matching quit-path fix.
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)) {
@@ -2966,9 +3033,11 @@ async function runSetup() {
2966
3033
  },
2967
3034
  };
2968
3035
 
3036
+ const existingIdentity = resolveExistingIdentityDefaults(NEXO_HOME);
3037
+
2969
3038
  // Detect language from input or use default
2970
- let lang = "en";
2971
- let t = i18n.en;
3039
+ let lang = existingIdentity.language || "en";
3040
+ let t = i18n[lang] || i18n.en;
2972
3041
  if (!useDefaults) {
2973
3042
  const langInput = await ask(" What's your preferred language? / ¿En qué idioma prefieres hablar?\n > ");
2974
3043
  const langLower = langInput.trim().toLowerCase();
@@ -3009,7 +3078,7 @@ async function runSetup() {
3009
3078
  // Step 2: User's name (P2) — v6.0.0 empty input falls through to "Usuario"
3010
3079
  // instead of keeping an empty string. The calibration file always ships
3011
3080
  // with a concrete user.name so downstream tooling does not need guards.
3012
- let userName = "Usuario";
3081
+ let userName = existingIdentity.userName || "Usuario";
3013
3082
  if (!useDefaults) {
3014
3083
  const nameInput = await ask(t.askUserName);
3015
3084
  const trimmedName = nameInput.trim();
@@ -3021,7 +3090,7 @@ async function runSetup() {
3021
3090
  }
3022
3091
 
3023
3092
  // Step 3: Agent name (P3)
3024
- let operatorName = DEFAULT_ASSISTANT_NAME;
3093
+ let operatorName = existingIdentity.operatorName || DEFAULT_ASSISTANT_NAME;
3025
3094
  if (!useDefaults) {
3026
3095
  while (true) {
3027
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.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",
@@ -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
- clean_plane = normalize_diagnostic_plane(plane)
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.diagnostic_plane_required",
52
+ id="orchestrator.diagnostic_plane_invalid",
48
53
  tier="orchestrator",
49
54
  status="critical",
50
55
  severity="error",
51
- summary="El diagnóstico está bloqueado hasta fijar explícitamente el plano",
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
- "NEXO mezcló planos en diagnósticos anteriores. El doctor no debe correr hasta que se elija "
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
 
@@ -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)