nexo-brain 6.5.0 → 7.1.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.
Files changed (95) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/README.md +6 -2
  3. package/bin/nexo-brain.js +310 -44
  4. package/bin/nexo.js +6 -0
  5. package/package.json +1 -1
  6. package/src/agent_runner.py +3 -2
  7. package/src/auto_close_sessions.py +9 -2
  8. package/src/auto_update.py +1216 -43
  9. package/src/automation_controls.py +814 -0
  10. package/src/bootstrap_docs.py +2 -1
  11. package/src/calibration_migration.py +5 -2
  12. package/src/classifier_local.py +43 -1
  13. package/src/cli.py +201 -22
  14. package/src/cli_email.py +213 -20
  15. package/src/client_preferences.py +7 -4
  16. package/src/client_sync.py +188 -115
  17. package/src/cron_recovery.py +16 -5
  18. package/src/crons/manifest.json +39 -0
  19. package/src/crons/sync.py +101 -20
  20. package/src/dashboard/app.py +9 -14
  21. package/src/db/__init__.py +1 -0
  22. package/src/db/_core.py +22 -9
  23. package/src/db/_email_accounts.py +93 -7
  24. package/src/db/_personal_scripts.py +122 -24
  25. package/src/db/_schema.py +73 -0
  26. package/src/db/_skills.py +145 -2
  27. package/src/desktop_bridge.py +33 -7
  28. package/src/doctor/providers/boot.py +21 -7
  29. package/src/doctor/providers/deep.py +6 -5
  30. package/src/doctor/providers/runtime.py +19 -18
  31. package/src/email_config.py +144 -33
  32. package/src/enforcement_engine.py +126 -5
  33. package/src/evolution_cycle.py +7 -6
  34. package/src/guardian_config.py +3 -3
  35. package/src/guardian_runtime_surfaces.py +248 -0
  36. package/src/guardian_telemetry.py +4 -1
  37. package/src/health_check.py +4 -2
  38. package/src/hook_guardrails.py +2 -0
  39. package/src/hooks/auto_capture.py +7 -2
  40. package/src/hooks/capture-session.sh +6 -2
  41. package/src/hooks/session-start.sh +41 -11
  42. package/src/hooks/session_start.py +5 -1
  43. package/src/paths.py +467 -0
  44. package/src/plugins/personal_plugins.py +14 -7
  45. package/src/plugins/personal_scripts.py +4 -1
  46. package/src/plugins/recover.py +3 -2
  47. package/src/plugins/schedule.py +4 -3
  48. package/src/plugins/update.py +68 -14
  49. package/src/presets/guardian_default.json +2 -2
  50. package/src/public_contribution.py +12 -7
  51. package/src/resonance_map.py +2 -0
  52. package/src/runtime_power.py +7 -6
  53. package/src/script_registry.py +469 -59
  54. package/src/scripts/backfill_task_owner.py +263 -0
  55. package/src/scripts/deep-sleep/apply_findings.py +70 -7
  56. package/src/scripts/deep-sleep/collect.py +73 -11
  57. package/src/scripts/deep-sleep/extract.py +19 -7
  58. package/src/scripts/deep-sleep/synthesize.py +3 -1
  59. package/src/scripts/nexo-auto-update.py +3 -2
  60. package/src/scripts/nexo-backup.sh +6 -3
  61. package/src/scripts/nexo-catchup.py +7 -6
  62. package/src/scripts/nexo-cognitive-decay.py +4 -3
  63. package/src/scripts/nexo-cortex-cycle.py +3 -2
  64. package/src/scripts/nexo-cron-wrapper.sh +6 -3
  65. package/src/scripts/nexo-daily-self-audit.py +55 -23
  66. package/src/scripts/nexo-deep-sleep.sh +6 -3
  67. package/src/scripts/nexo-email-migrate-config.py +111 -25
  68. package/src/scripts/nexo-email-monitor.py +2180 -0
  69. package/src/scripts/nexo-evolution-run.py +20 -17
  70. package/src/scripts/nexo-followup-hygiene.py +4 -3
  71. package/src/scripts/nexo-followup-runner.py +921 -0
  72. package/src/scripts/nexo-immune.py +7 -6
  73. package/src/scripts/nexo-impact-scorer.py +3 -2
  74. package/src/scripts/nexo-inbox-hook.sh +1 -1
  75. package/src/scripts/nexo-learning-housekeep.py +3 -2
  76. package/src/scripts/nexo-learning-validator.py +4 -3
  77. package/src/scripts/nexo-morning-agent.py +433 -0
  78. package/src/scripts/nexo-outcome-checker.py +3 -2
  79. package/src/scripts/nexo-postmortem-consolidator.py +10 -9
  80. package/src/scripts/nexo-pre-commit.py +2 -1
  81. package/src/scripts/nexo-proactive-dashboard.py +6 -4
  82. package/src/scripts/nexo-runtime-preflight.py +5 -4
  83. package/src/scripts/nexo-send-reply.py +483 -0
  84. package/src/scripts/nexo-sleep.py +9 -8
  85. package/src/scripts/nexo-snapshot-restore.sh +1 -1
  86. package/src/scripts/nexo-synthesis.py +7 -6
  87. package/src/scripts/nexo-tcc-approve.sh +2 -2
  88. package/src/scripts/nexo-watchdog-smoke.py +7 -5
  89. package/src/scripts/nexo-watchdog.sh +37 -26
  90. package/src/scripts/phase_guardian_analysis.py +3 -2
  91. package/src/state_watchers_runtime.py +6 -4
  92. package/src/system_catalog.py +23 -21
  93. package/src/tools_sessions.py +2 -1
  94. package/src/user_context.py +11 -5
  95. package/src/user_data_portability.py +26 -12
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "6.5.0",
3
+ "version": "7.1.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",
@@ -26,7 +26,7 @@
26
26
  "hooks": "./hooks/hooks.json",
27
27
  "userConfig": {
28
28
  "operator_name": {
29
- "description": "What should your AI co-operator call itself? (default: NEXO)",
29
+ "description": "What should your AI co-operator call itself? (default: Nova)",
30
30
  "sensitive": false
31
31
  }
32
32
  }
package/README.md CHANGED
@@ -18,7 +18,11 @@
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.5.0` is the current packaged-runtime line Plan Consolidado fase F0.2: operators can now `nexo scripts enable|disable|status <name>` any personal automation. The cron wrapper honours the flag at every tick (`exit 0` with `summary='[disabled]'` while the LaunchAgent stays loaded). The companion NEXO Desktop client (a closed-source product, distributed separately) wires the same toggle into its Automatizaciones panel. See [CHANGELOG](CHANGELOG.md) for the full diff.
21
+ Version `7.1.0` is the current packaged-runtime line. It closes the post-F0.6 runtime contract: `~/.nexo/core` is now the canonical shipped code root, layout healers re-run automatically after sync/update, Desktop consumes a Brain-generated Guardian runtime snapshot instead of stale manual lists, the core automation surface now officially includes `email-monitor`, `followup-runner`, and `morning-agent`, and the local classifier baseline auto-installs on fresh installs / `nexo update` unless the operator explicitly opts out with `NEXO_LOCAL_CLASSIFIER=off`. The companion NEXO Desktop client (v0.22.0, closed-source distributed separately) turns that same contract into a closed bootstrap/login/product flow.
22
+
23
+ Previously in `7.0.1`: hotfix over v7.0.0 (db._core.DB_PATH was only caller still hardcoded to legacy ~/.nexo/data/nexo.db; every shared-DB command silently returned empty results post-migration). Previously in `7.0.0`: **BREAKING — Plan Consolidado fase F0.6**: physical separation of the runtime tree into `~/.nexo/{core,personal,runtime}/`. The flat layout (`~/.nexo/scripts/`, `brain/`, `data/`, `operations/`, ...) is gone. Operators on v6.x are auto-migrated on first `nexo update`; fresh installs land directly in the new tree. New `paths.py` helpers are transition-aware.
24
+
25
+ Previously in `6.5.0`: Plan Consolidado fase F0.2: operators can now `nexo scripts enable|disable|status <name>` any personal automation. The cron wrapper honours the flag at every tick (`exit 0` with `summary='[disabled]'` while the LaunchAgent stays loaded). The companion NEXO Desktop client (a closed-source product, distributed separately) wires the same toggle into its Automatizaciones panel. See [CHANGELOG](CHANGELOG.md) for the full diff.
22
26
 
23
27
  > **About NEXO Desktop.** NEXO Desktop is a separate closed-source companion app distributed at [systeam.es/nexo-desktop](https://systeam.es/nexo-desktop) — its source does not live in this repo. When release notes mention Desktop they describe a coordinated client release that consumes the Brain's CLI / MCP contract; the Brain itself is fully usable on its own (terminal, Codex, Claude Code, or any MCP client).
24
28
 
@@ -792,7 +796,7 @@ npx nexo-brain
792
796
  The installer handles everything and syncs the same `nexo` MCP brain into Claude Code, Claude Desktop, and Codex when those clients are present:
793
797
 
794
798
  ```
795
- How should I call myself? (default: NEXO) > Atlas
799
+ How should I call myself? (default: Nova) > Atlas
796
800
 
797
801
  Can I explore your workspace to learn about your projects? (y/n) > y
798
802
 
package/bin/nexo-brain.js CHANGED
@@ -21,6 +21,23 @@ const path = require("path");
21
21
  const readline = require("readline");
22
22
 
23
23
  let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
24
+ const DEFAULT_ASSISTANT_NAME = "Nova";
25
+ const RESERVED_ASSISTANT_NAME_KEYS = new Set(["nexo", "nexobrain", "nexodesktop"]);
26
+
27
+ function normalizeAssistantNameCandidate(value) {
28
+ return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
29
+ }
30
+
31
+ function isReservedAssistantName(value) {
32
+ const normalized = normalizeAssistantNameCandidate(value);
33
+ if (!normalized) return false;
34
+ for (const reserved of RESERVED_ASSISTANT_NAME_KEYS) {
35
+ if (normalized === reserved || normalized.includes(reserved)) {
36
+ return true;
37
+ }
38
+ }
39
+ return false;
40
+ }
24
41
 
25
42
  function shouldSkipShellProfileBackfill() {
26
43
  // Mirror of _should_skip_shell_profile_backfill() in src/auto_update.py.
@@ -197,10 +214,11 @@ function isDuplicateArtifactName(name, dirPath = "") {
197
214
 
198
215
  function syncWatchdogHashRegistry(nexoHome) {
199
216
  try {
200
- const watchdogPath = path.join(nexoHome, "scripts", "nexo-watchdog.sh");
217
+ const scriptsDir = runtimeScriptsDir(nexoHome);
218
+ const watchdogPath = path.join(scriptsDir, "nexo-watchdog.sh");
201
219
  if (!fs.existsSync(watchdogPath)) return;
202
220
 
203
- const registryPath = path.join(nexoHome, "scripts", ".watchdog-hashes");
221
+ const registryPath = path.join(scriptsDir, ".watchdog-hashes");
204
222
  const entries = new Map();
205
223
  if (fs.existsSync(registryPath)) {
206
224
  for (const line of fs.readFileSync(registryPath, "utf8").split(/\r?\n/)) {
@@ -222,6 +240,40 @@ function syncWatchdogHashRegistry(nexoHome) {
222
240
  }
223
241
  }
224
242
 
243
+ function runtimeCodeDir(nexoHome) {
244
+ const coreDir = path.join(nexoHome, "core");
245
+ for (const candidate of [coreDir, nexoHome]) {
246
+ if (
247
+ fs.existsSync(path.join(candidate, "cli.py")) ||
248
+ fs.existsSync(path.join(candidate, "server.py")) ||
249
+ fs.existsSync(path.join(candidate, "db"))
250
+ ) {
251
+ return candidate;
252
+ }
253
+ }
254
+ return fs.existsSync(coreDir) ? coreDir : nexoHome;
255
+ }
256
+
257
+ function runtimeServerPath(nexoHome) {
258
+ const codeDir = runtimeCodeDir(nexoHome);
259
+ if (fs.existsSync(path.join(codeDir, "server.py"))) {
260
+ return path.join(codeDir, "server.py");
261
+ }
262
+ return path.join(nexoHome, "server.py");
263
+ }
264
+
265
+ function runtimeHooksDir(nexoHome) {
266
+ const codeDir = runtimeCodeDir(nexoHome);
267
+ const hooksDir = path.join(codeDir, "hooks");
268
+ return fs.existsSync(hooksDir) ? hooksDir : path.join(nexoHome, "hooks");
269
+ }
270
+
271
+ function runtimeScriptsDir(nexoHome) {
272
+ const codeDir = runtimeCodeDir(nexoHome);
273
+ const scriptsDir = path.join(codeDir, "scripts");
274
+ return fs.existsSync(scriptsDir) ? scriptsDir : path.join(nexoHome, "scripts");
275
+ }
276
+
225
277
  function writeRuntimeCoreArtifactsManifest(nexoHome, srcDir) {
226
278
  try {
227
279
  const listTopLevelFiles = (dirPath) => {
@@ -233,17 +285,23 @@ function writeRuntimeCoreArtifactsManifest(nexoHome, srcDir) {
233
285
  })
234
286
  .sort();
235
287
  };
236
- const configDir = path.join(nexoHome, "config");
237
- fs.mkdirSync(configDir, { recursive: true });
238
288
  const payload = {
239
289
  generated_at: new Date().toISOString(),
240
290
  script_names: listTopLevelFiles(path.join(srcDir, "scripts")),
241
291
  hook_names: listTopLevelFiles(path.join(srcDir, "hooks")),
242
292
  };
243
- fs.writeFileSync(
244
- path.join(configDir, "runtime-core-artifacts.json"),
245
- `${JSON.stringify(payload, null, 2)}\n`
246
- );
293
+ const manifestBody = `${JSON.stringify(payload, null, 2)}\n`;
294
+ const configDirs = [
295
+ path.join(nexoHome, "config"),
296
+ path.join(nexoHome, "personal", "config"),
297
+ ];
298
+ const seen = new Set();
299
+ for (const configDir of configDirs) {
300
+ if (seen.has(configDir)) continue;
301
+ seen.add(configDir);
302
+ fs.mkdirSync(configDir, { recursive: true });
303
+ fs.writeFileSync(path.join(configDir, "runtime-core-artifacts.json"), manifestBody);
304
+ }
247
305
  } catch (err) {
248
306
  log(`WARN: could not write runtime core-artifacts manifest: ${err.message}`);
249
307
  }
@@ -260,6 +318,44 @@ function syncRuntimePackageMetadata(repoRoot = path.join(__dirname, ".."), runti
260
318
  }
261
319
  }
262
320
 
321
+ function finalizeF06Layout(python, nexoHome = NEXO_HOME) {
322
+ try {
323
+ const result = spawnSync(
324
+ python,
325
+ [
326
+ "-c",
327
+ [
328
+ "import auto_update",
329
+ "auto_update._maybe_migrate_to_f06_layout()",
330
+ "auto_update._ensure_f06_legacy_shims()",
331
+ "auto_update._rewrite_f06_launch_agents()",
332
+ ].join("; "),
333
+ ],
334
+ {
335
+ cwd: nexoHome,
336
+ env: {
337
+ ...process.env,
338
+ NEXO_HOME: nexoHome,
339
+ NEXO_CODE: runtimeCodeDir(nexoHome),
340
+ PYTHONPATH: nexoHome,
341
+ },
342
+ encoding: "utf8",
343
+ },
344
+ );
345
+ if (result.status !== 0) {
346
+ const detail = (result.stderr || result.stdout || "").trim();
347
+ throw new Error(detail || "unknown error");
348
+ }
349
+ const marker = path.join(nexoHome, ".structure-version");
350
+ if (!fs.existsSync(marker) || fs.readFileSync(marker, "utf8").trim() !== "F0.6") {
351
+ throw new Error("F0.6 structure marker missing after layout finalization");
352
+ }
353
+ return { ok: true };
354
+ } catch (err) {
355
+ return { ok: false, error: String((err && err.message) || err) };
356
+ }
357
+ }
358
+
263
359
  function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
264
360
  const staticFiles = [
265
361
  "server.py",
@@ -648,8 +744,9 @@ function _hookCommand(hook, hooksDir, nexoHome) {
648
744
  }
649
745
 
650
746
  function _writeHooksStatus(nexoHome, manifestEntries, registrations) {
651
- // Publish ~/.nexo/hooks_status.json so NEXO Desktop can render the
652
- // "Hooks activos X/Y" widget without peeking into settings.json.
747
+ // Publish the canonical hook-health contract under runtime/operations/.
748
+ // Keep ~/.nexo/hooks_status.json only as a legacy alias so the root tree
749
+ // does not remain the live source of truth.
653
750
  try {
654
751
  const now = new Date();
655
752
  const pkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
@@ -664,19 +761,34 @@ function _writeHooksStatus(nexoHome, manifestEntries, registrations) {
664
761
  healthy,
665
762
  hooks: registrations,
666
763
  };
667
- fs.mkdirSync(nexoHome, { recursive: true });
668
- fs.writeFileSync(
669
- path.join(nexoHome, "hooks_status.json"),
670
- JSON.stringify(payload, null, 2) + "\n",
671
- );
764
+ const body = JSON.stringify(payload, null, 2) + "\n";
765
+ const canonicalDir = path.join(nexoHome, "runtime", "operations");
766
+ const canonicalPath = path.join(canonicalDir, "hooks_status.json");
767
+ const legacyPath = path.join(nexoHome, "hooks_status.json");
768
+
769
+ fs.mkdirSync(canonicalDir, { recursive: true });
770
+ fs.writeFileSync(canonicalPath, body);
771
+
772
+ try {
773
+ if (fs.existsSync(legacyPath) || fs.lstatSync(legacyPath).isSymbolicLink()) {
774
+ fs.rmSync(legacyPath, { force: true });
775
+ }
776
+ } catch (_) {}
777
+
778
+ try {
779
+ const relTarget = path.relative(path.dirname(legacyPath), canonicalPath) || path.basename(canonicalPath);
780
+ fs.symlinkSync(relTarget, legacyPath);
781
+ } catch (_) {
782
+ fs.writeFileSync(legacyPath, body);
783
+ }
672
784
  } catch (_) {}
673
785
  }
674
786
 
675
787
  /**
676
788
  * Register every hook declared by src/hooks/manifest.json into the
677
789
  * Claude Code settings file. Idempotent, never removes user-owned hooks.
678
- * Writes ~/.nexo/hooks_status.json after each run so NEXO Desktop can
679
- * display hook health without parsing settings.json.
790
+ * Writes the canonical hook-status contract after each run so NEXO Desktop
791
+ * can display hook health without parsing settings.json.
680
792
  */
681
793
  function registerAllCoreHooks(settings, hooksDir, nexoHome) {
682
794
  if (!settings.hooks) settings.hooks = {};
@@ -904,7 +1016,9 @@ function detectInstalledClients() {
904
1016
  ? [path.join(homeDir, "Applications", "Claude.app"), "/Applications/Claude.app"]
905
1017
  : [];
906
1018
  const desktopAppPath = desktopApps.find((candidate) => fs.existsSync(candidate)) || "";
907
- const claudeBin = run("which claude") || "";
1019
+ const managedClaudeBin = resolveManagedClaudeBinary();
1020
+ const persistedClaudeBin = readPersistedClaudeCliPath();
1021
+ const claudeBin = managedClaudeBin || persistedClaudeBin || run("which claude", { env: buildManagedCliEnv() }) || run("which claude") || "";
908
1022
  const codexBin = run("which codex") || "";
909
1023
  return {
910
1024
  claude_code: {
@@ -925,6 +1039,64 @@ function detectInstalledClients() {
925
1039
  };
926
1040
  }
927
1041
 
1042
+ function managedClaudePrefix() {
1043
+ const explicit = String(process.env.NEXO_CLAUDE_PREFIX || "").trim();
1044
+ if (explicit) return explicit;
1045
+ return path.join(NEXO_HOME, "runtime", "bootstrap", "npm-global");
1046
+ }
1047
+
1048
+ function buildManagedCliEnv(extraEnv = {}) {
1049
+ const prefix = managedClaudePrefix();
1050
+ const parts = [
1051
+ path.join(prefix, "bin"),
1052
+ process.env.PATH || "",
1053
+ ].filter(Boolean);
1054
+ return {
1055
+ ...process.env,
1056
+ npm_config_prefix: prefix,
1057
+ PATH: parts.join(path.delimiter),
1058
+ ...extraEnv,
1059
+ };
1060
+ }
1061
+
1062
+ function resolveManagedClaudeBinary() {
1063
+ const prefix = managedClaudePrefix();
1064
+ const candidates = process.platform === "win32"
1065
+ ? [path.join(prefix, "claude.cmd"), path.join(prefix, "bin", "claude.cmd")]
1066
+ : [path.join(prefix, "bin", "claude"), path.join(prefix, "claude")];
1067
+ return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || "";
1068
+ }
1069
+
1070
+ function readPersistedClaudeCliPath() {
1071
+ const candidates = [
1072
+ path.join(NEXO_HOME, "config", "claude-cli-path"),
1073
+ path.join(NEXO_HOME, "personal", "config", "claude-cli-path"),
1074
+ ];
1075
+ for (const file of candidates) {
1076
+ try {
1077
+ if (!fs.existsSync(file)) continue;
1078
+ const value = String(fs.readFileSync(file, "utf8") || "").trim();
1079
+ if (value && fs.existsSync(value)) return value;
1080
+ } catch {}
1081
+ }
1082
+ return "";
1083
+ }
1084
+
1085
+ function persistClaudeCliPath(claudePath) {
1086
+ const value = String(claudePath || "").trim();
1087
+ if (!value) return;
1088
+ const targets = [
1089
+ path.join(NEXO_HOME, "config", "claude-cli-path"),
1090
+ path.join(NEXO_HOME, "personal", "config", "claude-cli-path"),
1091
+ ];
1092
+ for (const file of targets) {
1093
+ try {
1094
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1095
+ fs.writeFileSync(file, value);
1096
+ } catch {}
1097
+ }
1098
+ }
1099
+
928
1100
  function clientSetupStrings(lang) {
929
1101
  if (lang === "es") {
930
1102
  return {
@@ -1092,17 +1264,48 @@ function requiredCliClients(setup) {
1092
1264
  }
1093
1265
 
1094
1266
  function installClaudeCodeCli(platform) {
1095
- let claudeInstalled = run("which claude");
1096
- if (claudeInstalled) return { installed: true, path: claudeInstalled };
1267
+ let claudeInstalled = detectInstalledClients().claude_code.path || "";
1268
+ if (claudeInstalled) {
1269
+ persistClaudeCliPath(claudeInstalled);
1270
+ return { installed: true, path: claudeInstalled };
1271
+ }
1272
+
1273
+ const installEnv = buildManagedCliEnv();
1274
+ const desktopNode = String(process.env.NEXO_DESKTOP_NODE || "").trim();
1275
+ const bundledNpmCli = String(process.env.NEXO_DESKTOP_NPM_CLI || "").trim();
1276
+ const managedPrefix = managedClaudePrefix();
1277
+
1278
+ if (desktopNode && bundledNpmCli) {
1279
+ spawnSync(
1280
+ desktopNode,
1281
+ [bundledNpmCli, "install", "-g", "--prefix", managedPrefix, "@anthropic-ai/claude-code"],
1282
+ {
1283
+ stdio: "inherit",
1284
+ env: { ...installEnv, ELECTRON_RUN_AS_NODE: "1" },
1285
+ },
1286
+ );
1287
+ claudeInstalled = detectInstalledClients().claude_code.path || "";
1288
+ if (claudeInstalled) {
1289
+ persistClaudeCliPath(claudeInstalled);
1290
+ return { installed: true, path: claudeInstalled };
1291
+ }
1292
+ }
1097
1293
 
1098
- spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], { stdio: "pipe", timeout: 60000 });
1099
- claudeInstalled = run("which claude");
1294
+ spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], {
1295
+ stdio: "pipe",
1296
+ timeout: 60000,
1297
+ env: installEnv,
1298
+ });
1299
+ claudeInstalled = detectInstalledClients().claude_code.path || "";
1100
1300
  if (!claudeInstalled) {
1101
1301
  const npmCmd = platform === "linux" ? "sudo" : "npm";
1102
- const npmArgs = platform === "linux" ? ["npm", "install", "-g", "@anthropic-ai/claude-code"] : ["install", "-g", "@anthropic-ai/claude-code"];
1103
- spawnSync(npmCmd, npmArgs, { stdio: "inherit" });
1104
- claudeInstalled = run("which claude");
1302
+ const npmArgs = platform === "linux"
1303
+ ? ["npm", "install", "-g", "@anthropic-ai/claude-code"]
1304
+ : ["install", "-g", "--prefix", managedPrefix, "@anthropic-ai/claude-code"];
1305
+ spawnSync(npmCmd, npmArgs, { stdio: "inherit", env: installEnv });
1306
+ claudeInstalled = detectInstalledClients().claude_code.path || "";
1105
1307
  }
1308
+ if (claudeInstalled) persistClaudeCliPath(claudeInstalled);
1106
1309
  return { installed: Boolean(claudeInstalled), path: claudeInstalled || "" };
1107
1310
  }
1108
1311
 
@@ -1642,6 +1845,7 @@ async function main() {
1642
1845
  || process.argv.includes("--yes")
1643
1846
  || process.argv.includes("--skip")
1644
1847
  || process.argv.includes("-y");
1848
+ const smokeTestMode = process.env.NEXO_TESTING_SMOKE === "1";
1645
1849
 
1646
1850
  console.log("");
1647
1851
  console.log(
@@ -1847,11 +2051,16 @@ async function main() {
1847
2051
  migrated_from: installedVersion,
1848
2052
  }, null, 2));
1849
2053
  syncRuntimePackageMetadata(path.join(__dirname, ".."), NEXO_HOME);
2054
+ log("Finalizing F0.6 runtime layout...");
2055
+ const migLayoutFinalize = finalizeF06Layout(migPython, NEXO_HOME);
2056
+ if (!migLayoutFinalize.ok) {
2057
+ throw new Error(`F0.6 layout finalization failed: ${migLayoutFinalize.error}`);
2058
+ }
1850
2059
 
1851
2060
  // Save updated CLAUDE.md template as reference (don't overwrite user's)
1852
2061
  const templateSrc = path.join(__dirname, "..", "templates", "CLAUDE.md.template");
1853
2062
  if (fs.existsSync(templateSrc)) {
1854
- const operatorName = installed.operator_name || "NEXO";
2063
+ const operatorName = installed.operator_name || DEFAULT_ASSISTANT_NAME;
1855
2064
  let claudeMd = fs.readFileSync(templateSrc, "utf8")
1856
2065
  .replace(/\{\{NAME\}\}/g, operatorName)
1857
2066
  .replace(/\{\{NEXO_HOME\}\}/g, NEXO_HOME);
@@ -1874,7 +2083,7 @@ async function main() {
1874
2083
  }
1875
2084
 
1876
2085
  // Restore operator shell alias + PATH if lost during previous updates
1877
- const migOperatorName = installed.operator_name || "NEXO";
2086
+ const migOperatorName = installed.operator_name || DEFAULT_ASSISTANT_NAME;
1878
2087
  const migAliasName = migOperatorName.toLowerCase();
1879
2088
  if (migAliasName !== "nexo") {
1880
2089
  const migSkip = shouldSkipShellProfileBackfill();
@@ -2003,6 +2212,11 @@ async function main() {
2003
2212
  }
2004
2213
  }
2005
2214
  }
2215
+ log("Finalizing F0.6 runtime layout...");
2216
+ const syncLayoutFinalize = finalizeF06Layout(syncPython, NEXO_HOME);
2217
+ if (!syncLayoutFinalize.ok) {
2218
+ throw new Error(`F0.6 layout finalization failed: ${syncLayoutFinalize.error}`);
2219
+ }
2006
2220
 
2007
2221
  logMacPermissionsNotice(NEXO_HOME, syncPython);
2008
2222
 
@@ -2069,8 +2283,9 @@ async function main() {
2069
2283
  dataDirConfirm: (p) => `Data directory: ${p}`,
2070
2284
  askUserName: " What's your name? > ",
2071
2285
  userGreet: (n) => `Nice to meet you, ${n}.`,
2072
- askAgentName: " What should I call myself? (default: NEXO) > ",
2286
+ askAgentName: ` What should I call myself? (default: ${DEFAULT_ASSISTANT_NAME}) > `,
2073
2287
  agentConfirm: (n) => `Got it. I'm ${n}.`,
2288
+ agentNameReserved: "That name is reserved for the product. Pick a different assistant name.",
2074
2289
  calibTitle: "Let's calibrate my personality to work best with you.",
2075
2290
  calibNote: "(You can change these anytime via nexo_preference_set)",
2076
2291
  autonomyQ: " How autonomous should I be?\n 1. Conservative — ask before most actions\n 2. Balanced — act on routine, ask on important\n 3. Full — act first, inform after, only ask when truly uncertain\n > ",
@@ -2101,8 +2316,9 @@ async function main() {
2101
2316
  dataDirConfirm: (p) => `Directorio de datos: ${p}`,
2102
2317
  askUserName: " ¿Cómo te llamas? > ",
2103
2318
  userGreet: (n) => `Encantado, ${n}.`,
2104
- askAgentName: " ¿Cómo quieres que me llame? (default: NEXO) > ",
2319
+ askAgentName: ` ¿Cómo quieres que me llame? (default: ${DEFAULT_ASSISTANT_NAME}) > `,
2105
2320
  agentConfirm: (n) => `Perfecto, soy ${n}.`,
2321
+ agentNameReserved: "Ese nombre está reservado para el producto. Elige otro nombre para el asistente.",
2106
2322
  calibTitle: "Vamos a calibrar mi personalidad para trabajar mejor contigo.",
2107
2323
  calibNote: "(Puedes cambiar esto en cualquier momento con nexo_preference_set)",
2108
2324
  autonomyQ: " ¿Cuánta autonomía me das?\n 1. Conservador — pregunto antes de casi todo\n 2. Equilibrado — actúo en lo rutinario, pregunto en lo importante\n 3. Total — actúo primero, informo después, solo pregunto si hay duda real\n > ",
@@ -2133,8 +2349,9 @@ async function main() {
2133
2349
  dataDirConfirm: (p) => `Répertoire de données : ${p}`,
2134
2350
  askUserName: " Comment tu t'appelles ? > ",
2135
2351
  userGreet: (n) => `Enchanté, ${n}.`,
2136
- askAgentName: " Comment veux-tu m'appeler ? (défaut: NEXO) > ",
2352
+ askAgentName: ` Comment veux-tu m'appeler ? (défaut : ${DEFAULT_ASSISTANT_NAME}) > `,
2137
2353
  agentConfirm: (n) => `C'est noté. Je suis ${n}.`,
2354
+ agentNameReserved: "Ce nom est réservé au produit. Choisis un autre nom pour l'assistant.",
2138
2355
  calibTitle: "Calibrons ma personnalité pour mieux travailler ensemble.",
2139
2356
  calibNote: "(Tu peux changer ça à tout moment avec nexo_preference_set)",
2140
2357
  autonomyQ: " Quel niveau d'autonomie me donnes-tu ?\n 1. Conservateur — je demande avant presque tout\n 2. Équilibré — j'agis en routine, je demande pour l'important\n 3. Total — j'agis d'abord, j'informe après\n > ",
@@ -2165,8 +2382,9 @@ async function main() {
2165
2382
  dataDirConfirm: (p) => `Datenverzeichnis: ${p}`,
2166
2383
  askUserName: " Wie heißt du? > ",
2167
2384
  userGreet: (n) => `Freut mich, ${n}.`,
2168
- askAgentName: " Wie soll ich heißen? (Standard: NEXO) > ",
2385
+ askAgentName: ` Wie soll ich heißen? (Standard: ${DEFAULT_ASSISTANT_NAME}) > `,
2169
2386
  agentConfirm: (n) => `Alles klar. Ich bin ${n}.`,
2387
+ agentNameReserved: "Dieser Name ist für das Produkt reserviert. Bitte wähle einen anderen Assistentennamen.",
2170
2388
  calibTitle: "Kalibrieren wir meine Persönlichkeit für die Zusammenarbeit.",
2171
2389
  calibNote: "(Jederzeit änderbar mit nexo_preference_set)",
2172
2390
  autonomyQ: " Wie viel Autonomie gibst du mir?\n 1. Konservativ — frage vor fast allem\n 2. Ausgewogen — handle bei Routine, frage bei Wichtigem\n 3. Voll — handle zuerst, informiere danach\n > ",
@@ -2197,8 +2415,9 @@ async function main() {
2197
2415
  dataDirConfirm: (p) => `Directory dati: ${p}`,
2198
2416
  askUserName: " Come ti chiami? > ",
2199
2417
  userGreet: (n) => `Piacere, ${n}.`,
2200
- askAgentName: " Come vuoi chiamarmi? (default: NEXO) > ",
2418
+ askAgentName: ` Come vuoi chiamarmi? (default: ${DEFAULT_ASSISTANT_NAME}) > `,
2201
2419
  agentConfirm: (n) => `Perfetto, sono ${n}.`,
2420
+ agentNameReserved: "Quel nome è riservato al prodotto. Scegli un altro nome per l'assistente.",
2202
2421
  calibTitle: "Calibriamo la mia personalità per lavorare meglio insieme.",
2203
2422
  calibNote: "(Puoi cambiare in qualsiasi momento con nexo_preference_set)",
2204
2423
  autonomyQ: " Quanta autonomia mi dai?\n 1. Conservatore — chiedo prima di quasi tutto\n 2. Equilibrato — agisco nella routine, chiedo per le cose importanti\n 3. Totale — agisco prima, informo dopo\n > ",
@@ -2229,8 +2448,9 @@ async function main() {
2229
2448
  dataDirConfirm: (p) => `Diretório de dados: ${p}`,
2230
2449
  askUserName: " Como te chamas? > ",
2231
2450
  userGreet: (n) => `Prazer, ${n}.`,
2232
- askAgentName: " Como queres que eu me chame? (padrão: NEXO) > ",
2451
+ askAgentName: ` Como queres que eu me chame? (padrão: ${DEFAULT_ASSISTANT_NAME}) > `,
2233
2452
  agentConfirm: (n) => `Perfeito, sou ${n}.`,
2453
+ agentNameReserved: "Esse nome está reservado para o produto. Escolhe outro nome para o assistente.",
2234
2454
  calibTitle: "Vamos calibrar a minha personalidade para trabalhar melhor contigo.",
2235
2455
  calibNote: "(Podes mudar a qualquer momento com nexo_preference_set)",
2236
2456
  autonomyQ: " Quanta autonomia me dás?\n 1. Conservador — pergunto antes de quase tudo\n 2. Equilibrado — ajo na rotina, pergunto no importante\n 3. Total — ajo primeiro, informo depois\n > ",
@@ -2312,8 +2532,19 @@ async function main() {
2312
2532
  }
2313
2533
 
2314
2534
  // Step 3: Agent name (P3)
2315
- const name = useDefaults ? "" : await ask(t.askAgentName);
2316
- const operatorName = name.trim() || "NEXO";
2535
+ let operatorName = DEFAULT_ASSISTANT_NAME;
2536
+ if (!useDefaults) {
2537
+ while (true) {
2538
+ const name = await ask(t.askAgentName);
2539
+ const candidate = name.trim() || DEFAULT_ASSISTANT_NAME;
2540
+ if (!isReservedAssistantName(candidate)) {
2541
+ operatorName = candidate;
2542
+ break;
2543
+ }
2544
+ log(t.agentNameReserved);
2545
+ console.log("");
2546
+ }
2547
+ }
2317
2548
  log(t.agentConfirm(operatorName));
2318
2549
  console.log("");
2319
2550
 
@@ -2446,6 +2677,24 @@ async function main() {
2446
2677
  );
2447
2678
  } catch (_) {}
2448
2679
 
2680
+ if (smokeTestMode) {
2681
+ // Pytest fresh-install smoke only needs to prove that the non-interactive
2682
+ // onboarding path writes the current calibration shape. Skip the rest of
2683
+ // the heavy bootstrap (client installs, pip, scan, LaunchAgents) so the
2684
+ // smoke does not sit on long dependency timeouts inside sandboxes.
2685
+ try {
2686
+ const canonicalBrainDir = path.join(NEXO_HOME, "personal", "brain");
2687
+ fs.mkdirSync(canonicalBrainDir, { recursive: true });
2688
+ fs.writeFileSync(
2689
+ path.join(canonicalBrainDir, "calibration.json"),
2690
+ JSON.stringify(calibration, null, 2)
2691
+ );
2692
+ } catch (_) {}
2693
+ log("Smoke test mode detected — wrote calibration and skipped heavy bootstrap.");
2694
+ rl.close();
2695
+ return;
2696
+ }
2697
+
2449
2698
  const clientConfig = await configureClientSetup({
2450
2699
  lang,
2451
2700
  useDefaults,
@@ -2596,10 +2845,18 @@ async function main() {
2596
2845
  ' printf \'%s\\n\' "${NEXO_CODE%/}"',
2597
2846
  ' return 0',
2598
2847
  ' fi',
2848
+ ' if [ -f "$NEXO_HOME/core/cli.py" ]; then',
2849
+ ' printf \'%s\\n\' "$NEXO_HOME/core"',
2850
+ ' return 0',
2851
+ ' fi',
2599
2852
  ' if [ -f "$NEXO_HOME/cli.py" ]; then',
2600
2853
  ' printf \'%s\\n\' "$NEXO_HOME"',
2601
2854
  ' return 0',
2602
2855
  ' fi',
2856
+ ' if [ -d "$NEXO_HOME/core" ]; then',
2857
+ ' printf \'%s\\n\' "$NEXO_HOME/core"',
2858
+ ' return 0',
2859
+ ' fi',
2603
2860
  ' printf \'%s\\n\' "$NEXO_HOME"',
2604
2861
  '}',
2605
2862
  'NEXO_CODE="$(resolve_code_dir)"',
@@ -2641,6 +2898,11 @@ async function main() {
2641
2898
  ' exit 1',
2642
2899
  'fi',
2643
2900
  'CLI_PY="$NEXO_CODE/cli.py"',
2901
+ 'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/core/cli.py" ]; then',
2902
+ ' NEXO_CODE="$NEXO_HOME/core"',
2903
+ ' export NEXO_CODE',
2904
+ ' CLI_PY="$NEXO_HOME/core/cli.py"',
2905
+ 'fi',
2644
2906
  'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/cli.py" ]; then',
2645
2907
  ' NEXO_CODE="$NEXO_HOME"',
2646
2908
  ' export NEXO_CODE',
@@ -3308,9 +3570,10 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
3308
3570
  if (!settings.mcpServers) settings.mcpServers = {};
3309
3571
  settings.mcpServers.nexo = {
3310
3572
  command: python,
3311
- args: [path.join(NEXO_HOME, "server.py")],
3573
+ args: [runtimeServerPath(NEXO_HOME)],
3312
3574
  env: {
3313
3575
  NEXO_HOME: NEXO_HOME,
3576
+ NEXO_CODE: runtimeCodeDir(NEXO_HOME),
3314
3577
  NEXO_NAME: operatorName,
3315
3578
  },
3316
3579
  };
@@ -3319,7 +3582,7 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
3319
3582
  if (!settings.hooks) settings.hooks = {};
3320
3583
 
3321
3584
  // Hook scripts already copied above — just reference the dest dir
3322
- const hooksDestDir = path.join(NEXO_HOME, "hooks");
3585
+ const hooksDestDir = runtimeHooksDir(NEXO_HOME);
3323
3586
 
3324
3587
  registerAllCoreHooks(settings, hooksDestDir, NEXO_HOME);
3325
3588
 
@@ -3328,12 +3591,12 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
3328
3591
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
3329
3592
  log("MCP server + 8 core hooks configured in Claude Code settings.");
3330
3593
 
3331
- const syncClientsScript = path.join(NEXO_HOME, "scripts", "nexo-sync-clients.py");
3594
+ const syncClientsScript = path.join(runtimeScriptsDir(NEXO_HOME), "nexo-sync-clients.py");
3332
3595
  if (fs.existsSync(syncClientsScript)) {
3333
3596
  const syncArgs = [
3334
3597
  syncClientsScript,
3335
3598
  "--nexo-home", NEXO_HOME,
3336
- "--runtime-root", NEXO_HOME,
3599
+ "--runtime-root", runtimeCodeDir(NEXO_HOME),
3337
3600
  "--python", python,
3338
3601
  "--operator-name", operatorName,
3339
3602
  ];
@@ -3374,11 +3637,9 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
3374
3637
  }
3375
3638
  }
3376
3639
 
3377
- const claudeCliPath = run("which claude") || "";
3640
+ const claudeCliPath = detectInstalledClients().claude_code.path || run("which claude", { env: buildManagedCliEnv() }) || run("which claude") || "";
3378
3641
  if (claudeCliPath) {
3379
- const cliPathFile = path.join(NEXO_HOME, "config", "claude-cli-path");
3380
- fs.mkdirSync(path.dirname(cliPathFile), { recursive: true });
3381
- fs.writeFileSync(cliPathFile, claudeCliPath.trim());
3642
+ persistClaudeCliPath(claudeCliPath.trim());
3382
3643
  log(`Claude CLI path saved: ${claudeCliPath.trim()}`);
3383
3644
  }
3384
3645
 
@@ -3391,7 +3652,6 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
3391
3652
  schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
3392
3653
  schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
3393
3654
  const enabledOptionals = { dashboard: doDashboard, automation: schedule.automation_enabled !== false };
3394
- const smokeTestMode = process.env.NEXO_TESTING_SMOKE === "1";
3395
3655
  if (smokeTestMode) {
3396
3656
  log("Smoke test mode detected — skipping LaunchAgents installation.");
3397
3657
  } else if (isEphemeralInstall(NEXO_HOME)) {
@@ -3515,6 +3775,12 @@ See ~/.nexo/ for configuration.
3515
3775
  log(`CLAUDE.md version tracker initialized: v${claudeMdVersionMatch[1]}`);
3516
3776
  }
3517
3777
 
3778
+ log("Finalizing F0.6 runtime layout...");
3779
+ const layoutFinalize = finalizeF06Layout(python, NEXO_HOME);
3780
+ if (!layoutFinalize.ok) {
3781
+ throw new Error(`F0.6 layout finalization failed: ${layoutFinalize.error}`);
3782
+ }
3783
+
3518
3784
  console.log("");
3519
3785
  const readyMsg = t.ready(operatorName, aliasName);
3520
3786
  const readySub = t.readySubtext;
package/bin/nexo.js CHANGED
@@ -42,12 +42,18 @@ function resolveCodeDir() {
42
42
  if (fs.existsSync(repoCandidate)) {
43
43
  return path.join(__dirname, "..", "src");
44
44
  }
45
+ if (fs.existsSync(path.join(NEXO_HOME, "core", "cli.py"))) {
46
+ return path.join(NEXO_HOME, "core");
47
+ }
45
48
  if (fs.existsSync(path.join(NEXO_HOME, "cli.py"))) {
46
49
  return NEXO_HOME;
47
50
  }
48
51
  if (fs.existsSync(path.join(NEXO_HOME, "claude", "cli.py"))) {
49
52
  return path.join(NEXO_HOME, "claude");
50
53
  }
54
+ if (fs.existsSync(path.join(NEXO_HOME, "core"))) {
55
+ return path.join(NEXO_HOME, "core");
56
+ }
51
57
  return NEXO_HOME;
52
58
  }
53
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "6.5.0",
3
+ "version": "7.1.0",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
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",