nexo-brain 7.12.1 → 7.12.3
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 +231 -25
- package/bin/nexo.js +10 -0
- package/bin/windows-wsl-bridge.js +0 -0
- package/package.json +2 -1
- package/src/agent_runner.py +0 -9
- package/src/auto_update.py +49 -0
- package/src/cli.py +15 -0
- package/src/client_preferences.py +17 -24
- package/src/email_config.py +0 -2
- package/src/health_check.py +58 -9
- package/src/local_models.py +5 -0
- package/src/requirements.txt +5 -2
- package/src/scripts/nexo-agent-run.py +5 -1
- package/src/scripts/nexo-email-migrate-config.py +0 -1
- package/src/scripts/nexo-email-monitor.py +16 -27
- package/src/scripts/nexo_personal_automation.py +4 -11
- package/src/support_snapshot.py +10 -2
- package/src/windows_runtime.py +327 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.3",
|
|
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.12.
|
|
21
|
+
Version `7.12.2` is the current packaged-runtime line. Patch release — legacy headless automation paths now stay on the resonance engine: `task_profile` no longer pre-fills model/effort, `email-monitor` stops carrying a private routing override, personal automation helpers stop injecting a default model, and runtime updates scrub the last stale email-profile field automatically. Result: email daemon, personal scripts, and updated installs all converge on the same `caller`/`tier` → backend → `(model, effort)` resolution path already used by Deep Sleep and morning-agent.
|
|
22
22
|
|
|
23
23
|
Previously in `7.12.0`: minor release — adds `nexo support-snapshot` for generic local runtime diagnostics and completes the silent-reminder hardening on the live Protocol Enforcer path. The support collector emits one JSON bundle with version/platform metadata, runtime path presence, health-check output, and recent event/operation tails, while map-driven reminders (`nexo_startup`, `nexo_smart_startup`, `nexo_heartbeat`, `nexo_reminders`, `nexo_session_diary_*`, `nexo_stop`, `nexo_task_close`, compaction checkpoint prompts) now say explicitly that silence owns the entire reminder turn.
|
|
24
24
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -17,8 +17,21 @@
|
|
|
17
17
|
const { execSync, spawnSync } = require("child_process");
|
|
18
18
|
const crypto = require("crypto");
|
|
19
19
|
const fs = require("fs");
|
|
20
|
+
const { createRequire } = require("module");
|
|
20
21
|
const path = require("path");
|
|
21
22
|
const readline = require("readline");
|
|
23
|
+
// Force relative launcher helpers to resolve from bin/ even under test harnesses.
|
|
24
|
+
require = createRequire(path.join(__dirname, "nexo-brain.js"));
|
|
25
|
+
const { runViaWsl } = require("./windows-wsl-bridge");
|
|
26
|
+
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
const bridged = runViaWsl({
|
|
29
|
+
scriptPath: __filename,
|
|
30
|
+
args: process.argv.slice(2),
|
|
31
|
+
label: "NEXO Brain",
|
|
32
|
+
});
|
|
33
|
+
process.exit(bridged?.status ?? 1);
|
|
34
|
+
}
|
|
22
35
|
|
|
23
36
|
let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
|
|
24
37
|
const DEFAULT_ASSISTANT_NAME = "Nova";
|
|
@@ -98,6 +111,10 @@ const DEFAULT_CLAUDE_CODE_REASONING_EFFORT = _MODEL_DEFAULTS.claude_code.reasoni
|
|
|
98
111
|
const DEFAULT_CODEX_MODEL = _MODEL_DEFAULTS.codex.model;
|
|
99
112
|
const DEFAULT_CODEX_REASONING_EFFORT = _MODEL_DEFAULTS.codex.reasoning_effort || "";
|
|
100
113
|
|
|
114
|
+
function isDesktopManagedInstall() {
|
|
115
|
+
return String(process.env.NEXO_DESKTOP_MANAGED || "").trim() === "1";
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
// v6.0.0 — Hook manifest is the single source of truth for which hook
|
|
102
119
|
// handlers get registered. Both plugin mode (hooks/hooks.json) and npm
|
|
103
120
|
// mode (this installer's registerAllCoreHooks) read from the same file.
|
|
@@ -210,6 +227,62 @@ function run(cmd, opts = {}) {
|
|
|
210
227
|
|
|
211
228
|
function log(msg) {
|
|
212
229
|
console.log(` ${msg}`);
|
|
230
|
+
try {
|
|
231
|
+
const logPath = path.join(
|
|
232
|
+
NEXO_HOME,
|
|
233
|
+
"runtime",
|
|
234
|
+
"bootstrap",
|
|
235
|
+
"logs",
|
|
236
|
+
"brain-install.log"
|
|
237
|
+
);
|
|
238
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
239
|
+
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${msg}\n`);
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let _stagedRuntimeBundleRoot = null;
|
|
244
|
+
let _stagedRuntimeCleanup = null;
|
|
245
|
+
|
|
246
|
+
function getRuntimeBundleRoot() {
|
|
247
|
+
if (_stagedRuntimeBundleRoot) {
|
|
248
|
+
return _stagedRuntimeBundleRoot;
|
|
249
|
+
}
|
|
250
|
+
const bundleRoot = path.resolve(__dirname, "..");
|
|
251
|
+
if (process.platform !== "linux") {
|
|
252
|
+
return bundleRoot;
|
|
253
|
+
}
|
|
254
|
+
const normalizedRoot = bundleRoot.replace(/\\/g, "/");
|
|
255
|
+
if (!normalizedRoot.startsWith("/mnt/")) {
|
|
256
|
+
return bundleRoot;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const tmpRoot = fs.mkdtempSync(path.join(require("os").tmpdir(), "nexo-brain-stage-"));
|
|
260
|
+
const stagedRoot = path.join(tmpRoot, path.basename(bundleRoot));
|
|
261
|
+
fs.mkdirSync(stagedRoot, { recursive: true });
|
|
262
|
+
|
|
263
|
+
log("Staging bundled runtime into local Linux storage...");
|
|
264
|
+
const tarCmd =
|
|
265
|
+
`tar --exclude=__pycache__ --exclude=node_modules --exclude='*.pyc' ` +
|
|
266
|
+
`--exclude='*.db' --exclude=.git ` +
|
|
267
|
+
`-C ${JSON.stringify(bundleRoot)} -cf - . | ` +
|
|
268
|
+
`tar -C ${JSON.stringify(stagedRoot)} -xf -`;
|
|
269
|
+
const stageResult = spawnSync("/bin/sh", ["-c", tarCmd], {
|
|
270
|
+
encoding: "utf8",
|
|
271
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
272
|
+
timeout: 10 * 60 * 1000,
|
|
273
|
+
});
|
|
274
|
+
if (stageResult.status !== 0) {
|
|
275
|
+
const detail = (stageResult.stderr || stageResult.stdout || "").trim() || `exit ${stageResult.status}`;
|
|
276
|
+
throw new Error(`Failed to stage bundled runtime from Windows storage: ${detail}`);
|
|
277
|
+
}
|
|
278
|
+
log(` Runtime bundle staged at ${stagedRoot}`);
|
|
279
|
+
_stagedRuntimeBundleRoot = stagedRoot;
|
|
280
|
+
_stagedRuntimeCleanup = () => {
|
|
281
|
+
try {
|
|
282
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
283
|
+
} catch {}
|
|
284
|
+
};
|
|
285
|
+
return _stagedRuntimeBundleRoot;
|
|
213
286
|
}
|
|
214
287
|
|
|
215
288
|
function readPackageJson() {
|
|
@@ -487,6 +560,15 @@ function runMandatoryModelWarmup(pythonPath, nexoHome = NEXO_HOME, { reason = "i
|
|
|
487
560
|
log("Local model warmup complete.");
|
|
488
561
|
}
|
|
489
562
|
|
|
563
|
+
function runDesktopAwareModelWarmup(pythonPath, nexoHome = NEXO_HOME, options = {}) {
|
|
564
|
+
const reason = String((options && options.reason) || "install");
|
|
565
|
+
if (isDesktopManagedInstall()) {
|
|
566
|
+
log(`Desktop-managed runtime detected — local model warmup deferred during ${reason}.`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
runMandatoryModelWarmup(pythonPath, nexoHome, options);
|
|
570
|
+
}
|
|
571
|
+
|
|
490
572
|
async function runWarmupModelsCommand(args) {
|
|
491
573
|
const dryRun = args.includes("--dry-run");
|
|
492
574
|
const json = args.includes("--json");
|
|
@@ -716,6 +798,10 @@ function resolveRuntimeSchedulePath(nexoHome) {
|
|
|
716
798
|
|
|
717
799
|
function finalizeF06Layout(python, nexoHome = NEXO_HOME) {
|
|
718
800
|
try {
|
|
801
|
+
// auto_update lives in NEXO_CODE (e.g. ~/.nexo/core or ~/.nexo/core/current),
|
|
802
|
+
// not directly in NEXO_HOME, so PYTHONPATH must point at the runtime code
|
|
803
|
+
// directory or `import auto_update` fails with ModuleNotFoundError.
|
|
804
|
+
const codeDir = runtimeCodeDir(nexoHome);
|
|
719
805
|
const result = spawnSync(
|
|
720
806
|
python,
|
|
721
807
|
[
|
|
@@ -731,8 +817,8 @@ function finalizeF06Layout(python, nexoHome = NEXO_HOME) {
|
|
|
731
817
|
env: {
|
|
732
818
|
...process.env,
|
|
733
819
|
NEXO_HOME: nexoHome,
|
|
734
|
-
NEXO_CODE:
|
|
735
|
-
PYTHONPATH:
|
|
820
|
+
NEXO_CODE: codeDir,
|
|
821
|
+
PYTHONPATH: codeDir,
|
|
736
822
|
},
|
|
737
823
|
encoding: "utf8",
|
|
738
824
|
},
|
|
@@ -1438,7 +1524,7 @@ function getDefaultSchedule(timezone) {
|
|
|
1438
1524
|
}
|
|
1439
1525
|
|
|
1440
1526
|
function writeDesktopProductMode(nexoHome) {
|
|
1441
|
-
if (
|
|
1527
|
+
if (!isDesktopManagedInstall()) return;
|
|
1442
1528
|
const configDir = resolveRuntimeConfigDir(nexoHome);
|
|
1443
1529
|
fs.mkdirSync(configDir, { recursive: true });
|
|
1444
1530
|
const target = path.join(configDir, "product-mode.json");
|
|
@@ -1465,7 +1551,7 @@ function ensureEvolutionObjectiveForCurrentProductMode(nexoHome) {
|
|
|
1465
1551
|
const brainDir = resolveRuntimeBrainDir(nexoHome);
|
|
1466
1552
|
fs.mkdirSync(brainDir, { recursive: true });
|
|
1467
1553
|
const evoObjectivePath = path.join(brainDir, "evolution-objective.json");
|
|
1468
|
-
const desktopManaged =
|
|
1554
|
+
const desktopManaged = isDesktopManagedInstall();
|
|
1469
1555
|
let payload = null;
|
|
1470
1556
|
if (fs.existsSync(evoObjectivePath)) {
|
|
1471
1557
|
try {
|
|
@@ -1788,6 +1874,29 @@ function installClaudeCodeCli(platform) {
|
|
|
1788
1874
|
const desktopNode = String(process.env.NEXO_DESKTOP_NODE || "").trim();
|
|
1789
1875
|
const bundledNpmCli = String(process.env.NEXO_DESKTOP_NPM_CLI || "").trim();
|
|
1790
1876
|
const managedPrefix = managedClaudePrefix();
|
|
1877
|
+
const desktopManaged = isDesktopManagedInstall();
|
|
1878
|
+
|
|
1879
|
+
// OFFLINE-FIRST: detect bundled claude-code .tgz and install from it.
|
|
1880
|
+
// Path: resources/brain-bundle/claude-code/claude-code-X.Y.Z.tgz (relative
|
|
1881
|
+
// to bin/, so __dirname/../claude-code/). Falls back to PyPI/npm if absent.
|
|
1882
|
+
const bundledClaudeDir = path.join(__dirname, "..", "claude-code");
|
|
1883
|
+
if (fs.existsSync(bundledClaudeDir)) {
|
|
1884
|
+
const tgzFiles = fs.readdirSync(bundledClaudeDir).filter((f) => f.endsWith(".tgz"));
|
|
1885
|
+
if (tgzFiles.length > 0) {
|
|
1886
|
+
const tgzPath = path.join(bundledClaudeDir, tgzFiles[0]);
|
|
1887
|
+
log(" Installing claude-code from bundled tarball (offline)...");
|
|
1888
|
+
spawnSync(
|
|
1889
|
+
"npm",
|
|
1890
|
+
["install", "-g", "--prefix", managedPrefix, tgzPath],
|
|
1891
|
+
{ stdio: "inherit", env: installEnv },
|
|
1892
|
+
);
|
|
1893
|
+
claudeInstalled = detectInstalledClients().claude_code.path || "";
|
|
1894
|
+
if (claudeInstalled) {
|
|
1895
|
+
persistClaudeCliPath(claudeInstalled);
|
|
1896
|
+
return { installed: true, path: claudeInstalled };
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1791
1900
|
|
|
1792
1901
|
if (desktopNode && bundledNpmCli) {
|
|
1793
1902
|
spawnSync(
|
|
@@ -1805,6 +1914,23 @@ function installClaudeCodeCli(platform) {
|
|
|
1805
1914
|
}
|
|
1806
1915
|
}
|
|
1807
1916
|
|
|
1917
|
+
if (desktopManaged) {
|
|
1918
|
+
spawnSync(
|
|
1919
|
+
"npm",
|
|
1920
|
+
["install", "-g", "--prefix", managedPrefix, "@anthropic-ai/claude-code"],
|
|
1921
|
+
{
|
|
1922
|
+
stdio: "inherit",
|
|
1923
|
+
env: installEnv,
|
|
1924
|
+
},
|
|
1925
|
+
);
|
|
1926
|
+
claudeInstalled = detectInstalledClients().claude_code.path || "";
|
|
1927
|
+
if (claudeInstalled) {
|
|
1928
|
+
persistClaudeCliPath(claudeInstalled);
|
|
1929
|
+
return { installed: true, path: claudeInstalled };
|
|
1930
|
+
}
|
|
1931
|
+
return { installed: false, path: "" };
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1808
1934
|
spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], {
|
|
1809
1935
|
stdio: "pipe",
|
|
1810
1936
|
timeout: 60000,
|
|
@@ -1834,6 +1960,7 @@ function installCodexCli() {
|
|
|
1834
1960
|
async function configureClientSetup({ lang, useDefaults, autoInstall, detected }) {
|
|
1835
1961
|
const strings = clientSetupStrings(lang);
|
|
1836
1962
|
const setup = defaultClientSetup(detected);
|
|
1963
|
+
const desktopManaged = String(process.env.NEXO_DESKTOP_MANAGED || "").trim() === "1";
|
|
1837
1964
|
setup.client_install_preferences = {
|
|
1838
1965
|
claude_code: autoInstall === "auto" ? "auto" : "ask",
|
|
1839
1966
|
codex: autoInstall === "auto" ? "auto" : "ask",
|
|
@@ -1885,6 +2012,10 @@ async function configureClientSetup({ lang, useDefaults, autoInstall, detected }
|
|
|
1885
2012
|
const required = requiredCliClients(setup);
|
|
1886
2013
|
for (const client of required) {
|
|
1887
2014
|
if (detected[client] && detected[client].installed) continue;
|
|
2015
|
+
if (desktopManaged && client === "claude_code") {
|
|
2016
|
+
log("Claude Code install deferred to Desktop final sync.");
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
1888
2019
|
let shouldInstall = useDefaults || autoInstall === "auto";
|
|
1889
2020
|
if (!shouldInstall && process.stdin.isTTY && process.stdout.isTTY) {
|
|
1890
2021
|
const question = client === "claude_code" ? strings.installClaudeQ : strings.installCodexQ;
|
|
@@ -1913,6 +2044,10 @@ async function configureClientSetup({ lang, useDefaults, autoInstall, detected }
|
|
|
1913
2044
|
}
|
|
1914
2045
|
|
|
1915
2046
|
if (setup.automation_enabled && setup.automation_backend !== "none" && !detected[setup.automation_backend]?.installed) {
|
|
2047
|
+
if (desktopManaged && setup.automation_backend === "claude_code") {
|
|
2048
|
+
log("Claude Code will be provisioned by Desktop after the core runtime is ready.");
|
|
2049
|
+
return { setup, detected };
|
|
2050
|
+
}
|
|
1916
2051
|
const label = setup.automation_backend === "claude_code" ? "Claude Code" : "Codex";
|
|
1917
2052
|
log(strings.automationDisabled(label));
|
|
1918
2053
|
setup.automation_enabled = false;
|
|
@@ -2425,9 +2560,9 @@ async function runSetup() {
|
|
|
2425
2560
|
// Check prerequisites
|
|
2426
2561
|
const platform = process.platform;
|
|
2427
2562
|
if (platform === "win32") {
|
|
2428
|
-
log("Windows detected
|
|
2563
|
+
log("Windows detected, but the automatic WSL bridge was not available.");
|
|
2429
2564
|
log("Install WSL: https://learn.microsoft.com/en-us/windows/wsl/install");
|
|
2430
|
-
log("Then run this command inside WSL (Ubuntu terminal)
|
|
2565
|
+
log("Then run this command again, or launch it directly inside WSL (Ubuntu terminal).");
|
|
2431
2566
|
process.exit(1);
|
|
2432
2567
|
}
|
|
2433
2568
|
if (platform !== "darwin" && platform !== "linux") {
|
|
@@ -2435,6 +2570,15 @@ async function runSetup() {
|
|
|
2435
2570
|
process.exit(1);
|
|
2436
2571
|
}
|
|
2437
2572
|
|
|
2573
|
+
const bundleRoot = getRuntimeBundleRoot();
|
|
2574
|
+
const bundleSrcDir = path.join(bundleRoot, "src");
|
|
2575
|
+
const bundleTemplatesDir = path.join(bundleRoot, "templates");
|
|
2576
|
+
process.on("exit", () => {
|
|
2577
|
+
if (_stagedRuntimeCleanup) {
|
|
2578
|
+
_stagedRuntimeCleanup();
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2438
2582
|
const onboardingMigration = ensureOnboardingCompletionMarker(NEXO_HOME);
|
|
2439
2583
|
if (onboardingMigration.changed) {
|
|
2440
2584
|
log("Migrated legacy calibration completion marker.");
|
|
@@ -2466,7 +2610,7 @@ async function runSetup() {
|
|
|
2466
2610
|
}
|
|
2467
2611
|
|
|
2468
2612
|
// Recursive copy helper (skips __pycache__, .pyc, .db files)
|
|
2469
|
-
const srcDir =
|
|
2613
|
+
const srcDir = bundleSrcDir;
|
|
2470
2614
|
const copyDirRec = (src, dest) => {
|
|
2471
2615
|
fs.mkdirSync(dest, { recursive: true });
|
|
2472
2616
|
fs.readdirSync(src).forEach(item => {
|
|
@@ -2539,7 +2683,7 @@ async function runSetup() {
|
|
|
2539
2683
|
}
|
|
2540
2684
|
|
|
2541
2685
|
const migPythonForWarmup = findVenvPython(NEXO_HOME) || "python3";
|
|
2542
|
-
|
|
2686
|
+
runDesktopAwareModelWarmup(migPythonForWarmup, NEXO_HOME, { reason: "update", installRuntimeDeps: false });
|
|
2543
2687
|
|
|
2544
2688
|
// Update plugins (all .py files in plugins/)
|
|
2545
2689
|
const pluginsSrc = path.join(srcDir, "plugins");
|
|
@@ -2594,7 +2738,7 @@ async function runSetup() {
|
|
|
2594
2738
|
// hand-edited template under ~/.nexo/templates/ is replaced on
|
|
2595
2739
|
// upgrade. Keep local forks under personal/ or outside the runtime
|
|
2596
2740
|
// home to avoid silent loss.
|
|
2597
|
-
const migTemplatesSrc =
|
|
2741
|
+
const migTemplatesSrc = bundleTemplatesDir;
|
|
2598
2742
|
const migTemplatesDest = path.join(NEXO_HOME, "templates");
|
|
2599
2743
|
if (fs.existsSync(migTemplatesSrc)) {
|
|
2600
2744
|
copyDirRec(migTemplatesSrc, migTemplatesDest);
|
|
@@ -2643,7 +2787,7 @@ async function runSetup() {
|
|
|
2643
2787
|
? { runtime_repaired_from: activeRuntimeVersion }
|
|
2644
2788
|
: {}),
|
|
2645
2789
|
}, null, 2));
|
|
2646
|
-
syncRuntimePackageMetadata(
|
|
2790
|
+
syncRuntimePackageMetadata(bundleRoot, NEXO_HOME);
|
|
2647
2791
|
log("Finalizing F0.6 runtime layout...");
|
|
2648
2792
|
const migLayoutFinalize = finalizeF06Layout(migPython, NEXO_HOME);
|
|
2649
2793
|
if (!migLayoutFinalize.ok) {
|
|
@@ -2657,7 +2801,7 @@ async function runSetup() {
|
|
|
2657
2801
|
|
|
2658
2802
|
// Keep the rendered template in-memory for version tracking, but do
|
|
2659
2803
|
// not drop a loose reference file in NEXO_HOME root.
|
|
2660
|
-
const templateSrc = path.join(
|
|
2804
|
+
const templateSrc = path.join(bundleTemplatesDir, "CLAUDE.md.template");
|
|
2661
2805
|
if (fs.existsSync(templateSrc)) {
|
|
2662
2806
|
const operatorName = installed.operator_name || DEFAULT_ASSISTANT_NAME;
|
|
2663
2807
|
let claudeMd = fs.readFileSync(templateSrc, "utf8")
|
|
@@ -2729,7 +2873,7 @@ async function runSetup() {
|
|
|
2729
2873
|
// Same version — backfill crons/ if missing (for installs before crons was shipped)
|
|
2730
2874
|
const syncPython = findVenvPython(NEXO_HOME) || run("which python3") || "python3";
|
|
2731
2875
|
const cronsDest = resolveRuntimeCronsDir(NEXO_HOME);
|
|
2732
|
-
const cronsSrc = path.join(
|
|
2876
|
+
const cronsSrc = path.join(bundleSrcDir, "crons");
|
|
2733
2877
|
if (fs.existsSync(cronsSrc)) {
|
|
2734
2878
|
const copyDirRec2 = (src, dest) => {
|
|
2735
2879
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -2754,7 +2898,7 @@ async function runSetup() {
|
|
|
2754
2898
|
|
|
2755
2899
|
// Same version — refresh packaged core skills/templates/runtime helpers too.
|
|
2756
2900
|
const skillsCoreDest = path.join(NEXO_HOME, "core", "skills");
|
|
2757
|
-
const skillsCoreSrc = path.join(
|
|
2901
|
+
const skillsCoreSrc = path.join(bundleSrcDir, "skills");
|
|
2758
2902
|
if (fs.existsSync(skillsCoreSrc)) {
|
|
2759
2903
|
const copyDirRec3 = (src, dest) => {
|
|
2760
2904
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -2771,16 +2915,16 @@ async function runSetup() {
|
|
|
2771
2915
|
}
|
|
2772
2916
|
|
|
2773
2917
|
["skills_runtime.py"].forEach((fname) => {
|
|
2774
|
-
const srcFile = path.join(
|
|
2918
|
+
const srcFile = path.join(bundleSrcDir, fname);
|
|
2775
2919
|
const destFile = path.join(NEXO_HOME, "core", fname);
|
|
2776
2920
|
if (fs.existsSync(srcFile)) {
|
|
2777
2921
|
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
2778
2922
|
fs.copyFileSync(srcFile, destFile);
|
|
2779
2923
|
}
|
|
2780
2924
|
});
|
|
2781
|
-
syncRuntimePackageMetadata(
|
|
2925
|
+
syncRuntimePackageMetadata(bundleRoot, NEXO_HOME);
|
|
2782
2926
|
|
|
2783
|
-
const templatesSrc =
|
|
2927
|
+
const templatesSrc = bundleTemplatesDir;
|
|
2784
2928
|
const templatesDest = path.join(NEXO_HOME, "templates");
|
|
2785
2929
|
if (fs.existsSync(templatesSrc)) {
|
|
2786
2930
|
fs.mkdirSync(templatesDest, { recursive: true });
|
|
@@ -2808,7 +2952,7 @@ async function runSetup() {
|
|
|
2808
2952
|
throw new Error(`F0.6 layout finalization failed: ${syncLayoutFinalize.error}`);
|
|
2809
2953
|
}
|
|
2810
2954
|
|
|
2811
|
-
|
|
2955
|
+
runDesktopAwareModelWarmup(syncPython, NEXO_HOME, { reason: "repair" });
|
|
2812
2956
|
logMacPermissionsNotice(NEXO_HOME, syncPython);
|
|
2813
2957
|
|
|
2814
2958
|
log(`Already at v${currentVersion}. No migration needed.`);
|
|
@@ -3303,11 +3447,22 @@ async function runSetup() {
|
|
|
3303
3447
|
// Use venv python if available, otherwise fall back to system python with --break-system-packages
|
|
3304
3448
|
const pipPython = fs.existsSync(venvPython) ? venvPython : python;
|
|
3305
3449
|
const requirementsFile = path.join(__dirname, "..", "src", "requirements.txt");
|
|
3306
|
-
|
|
3450
|
+
// Detect bundled wheels in resources/python-wheels (offline-first). If
|
|
3451
|
+
// present, pip uses --no-index --find-links to install without internet.
|
|
3452
|
+
// Falls back to PyPI if bundle not found.
|
|
3453
|
+
const bundledWheelsDir = path.join(__dirname, "..", "python-wheels");
|
|
3454
|
+
const useBundle = fs.existsSync(bundledWheelsDir);
|
|
3455
|
+
const pipArgs = useBundle
|
|
3456
|
+
? ["-m", "pip", "install", "--no-index", "--find-links", bundledWheelsDir, "--progress-bar", "off", "-r", requirementsFile]
|
|
3457
|
+
: ["-m", "pip", "install", "-v", "--progress-bar", "off", "--default-timeout=60", "-r", requirementsFile];
|
|
3307
3458
|
if (!fs.existsSync(venvPython)) {
|
|
3308
|
-
pipArgs.push("--break-system-packages");
|
|
3459
|
+
pipArgs.push("--break-system-packages");
|
|
3460
|
+
}
|
|
3461
|
+
if (useBundle) {
|
|
3462
|
+
log(" Installing Python deps from bundled wheels (offline)...");
|
|
3463
|
+
} else {
|
|
3464
|
+
log(" Installing Python deps from PyPI (online)...");
|
|
3309
3465
|
}
|
|
3310
|
-
|
|
3311
3466
|
const pipInstall = spawnSync(pipPython, pipArgs, { stdio: "inherit" });
|
|
3312
3467
|
if (pipInstall.status !== 0) {
|
|
3313
3468
|
log("Failed to install Python dependencies.");
|
|
@@ -3319,7 +3474,54 @@ async function runSetup() {
|
|
|
3319
3474
|
python = venvPython;
|
|
3320
3475
|
}
|
|
3321
3476
|
log("Dependencies installed.");
|
|
3322
|
-
|
|
3477
|
+
|
|
3478
|
+
// OFFLINE-FIRST: copy bundled LLM models to runtime/models BEFORE warmup,
|
|
3479
|
+
// so fastembed finds them locally and skips the ~217MB HuggingFace download.
|
|
3480
|
+
// Bundle layout: resources/brain-bundle/models/<source-repo-name>/<all files>.
|
|
3481
|
+
// Target layout: <NEXO_HOME>/runtime/models/<spec.name slugified>/<revision>/<files>.
|
|
3482
|
+
// We map by source_repo basename to match local_model_manifest.json.
|
|
3483
|
+
const bundledModelsDir = path.join(__dirname, "..", "models");
|
|
3484
|
+
if (fs.existsSync(bundledModelsDir)) {
|
|
3485
|
+
try {
|
|
3486
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "src", "local_model_manifest.json"), "utf8"));
|
|
3487
|
+
const runtimeModelsDir = path.join(NEXO_HOME, "runtime", "models");
|
|
3488
|
+
let modelsCopied = 0;
|
|
3489
|
+
for (const spec of manifest.models || []) {
|
|
3490
|
+
// Bundle layout supports either model_id basename (e.g.
|
|
3491
|
+
// "bge-base-en-v1.5" from "BAAI/bge-base-en-v1.5") or source_repo
|
|
3492
|
+
// basename (e.g. "bge-base-en-v1.5-onnx-q" from "qdrant/...").
|
|
3493
|
+
const modelIdName = (spec.model_id || "").split("/").pop();
|
|
3494
|
+
const sourceRepoName = (spec.source_repo || "").split("/").pop();
|
|
3495
|
+
let sourceDir = path.join(bundledModelsDir, modelIdName);
|
|
3496
|
+
if (!fs.existsSync(sourceDir)) {
|
|
3497
|
+
sourceDir = path.join(bundledModelsDir, sourceRepoName);
|
|
3498
|
+
}
|
|
3499
|
+
if (!fs.existsSync(sourceDir)) continue;
|
|
3500
|
+
const slug = (spec.name || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3501
|
+
const targetDir = path.join(runtimeModelsDir, slug, spec.revision);
|
|
3502
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
3503
|
+
for (const f of (spec.required_files || [])) {
|
|
3504
|
+
const src = path.join(sourceDir, f.path);
|
|
3505
|
+
const dst = path.join(targetDir, f.path);
|
|
3506
|
+
if (fs.existsSync(src) && !fs.existsSync(dst)) {
|
|
3507
|
+
fs.copyFileSync(src, dst);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
// Write the lock file to match revision (avoids re-download).
|
|
3511
|
+
fs.writeFileSync(path.join(targetDir, ".nexo-model-lock.json"), JSON.stringify({
|
|
3512
|
+
name: spec.name, kind: spec.kind, model_id: spec.model_id,
|
|
3513
|
+
source_repo: spec.source_repo, revision: spec.revision, model_file: spec.model_file,
|
|
3514
|
+
required_files: spec.required_files,
|
|
3515
|
+
}, null, 2));
|
|
3516
|
+
modelsCopied++;
|
|
3517
|
+
}
|
|
3518
|
+
if (modelsCopied > 0) log(` Copied ${modelsCopied} pre-bundled LLM model(s) (offline).`);
|
|
3519
|
+
} catch (err) {
|
|
3520
|
+
log(` WARN: bundled models copy failed: ${err.message}`);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
runDesktopAwareModelWarmup(python, NEXO_HOME, { reason: "install", installRuntimeDeps: false });
|
|
3323
3525
|
|
|
3324
3526
|
// Step 4: Create ~/.nexo/
|
|
3325
3527
|
log("Setting up NEXO home...");
|
|
@@ -3368,15 +3570,15 @@ async function runSetup() {
|
|
|
3368
3570
|
files_updated: 0,
|
|
3369
3571
|
}, null, 2)
|
|
3370
3572
|
);
|
|
3371
|
-
syncRuntimePackageMetadata(
|
|
3573
|
+
syncRuntimePackageMetadata(bundleRoot, NEXO_HOME);
|
|
3372
3574
|
|
|
3373
3575
|
// Copy source files
|
|
3374
3576
|
log("Copying core runtime files...");
|
|
3375
|
-
const srcDir =
|
|
3577
|
+
const srcDir = bundleSrcDir;
|
|
3376
3578
|
const pluginsSrcDir = path.join(srcDir, "plugins");
|
|
3377
3579
|
const scriptsSrcDir = path.join(srcDir, "scripts");
|
|
3378
3580
|
const skillsSrcDir = path.join(srcDir, "skills");
|
|
3379
|
-
const templateDir =
|
|
3581
|
+
const templateDir = bundleTemplatesDir;
|
|
3380
3582
|
|
|
3381
3583
|
// Recursive copy helper (skips __pycache__, .pyc, .db files)
|
|
3382
3584
|
const copyDirRecursive = (src, dest) => {
|
|
@@ -3394,7 +3596,7 @@ async function runSetup() {
|
|
|
3394
3596
|
};
|
|
3395
3597
|
|
|
3396
3598
|
// Core flat files (single .py files in src/)
|
|
3397
|
-
const coreFiles = getCoreRuntimeFlatFiles();
|
|
3599
|
+
const coreFiles = getCoreRuntimeFlatFiles(srcDir);
|
|
3398
3600
|
coreFiles.forEach((f) => {
|
|
3399
3601
|
const src = path.join(srcDir, f);
|
|
3400
3602
|
if (fs.existsSync(src)) {
|
|
@@ -4416,6 +4618,10 @@ See ~/.nexo/ for configuration.
|
|
|
4416
4618
|
console.log(` \u255A${"═".repeat(bw - 2)}\u255D`);
|
|
4417
4619
|
console.log("");
|
|
4418
4620
|
|
|
4621
|
+
if (_stagedRuntimeCleanup) {
|
|
4622
|
+
_stagedRuntimeCleanup();
|
|
4623
|
+
_stagedRuntimeCleanup = null;
|
|
4624
|
+
}
|
|
4419
4625
|
closeReadline();
|
|
4420
4626
|
}
|
|
4421
4627
|
|
package/bin/nexo.js
CHANGED
|
@@ -11,6 +11,16 @@ const { spawnSync } = require("child_process");
|
|
|
11
11
|
const fs = require("fs");
|
|
12
12
|
const os = require("os");
|
|
13
13
|
const path = require("path");
|
|
14
|
+
const { runViaWsl } = require("./windows-wsl-bridge");
|
|
15
|
+
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
const bridged = runViaWsl({
|
|
18
|
+
scriptPath: __filename,
|
|
19
|
+
args: process.argv.slice(2),
|
|
20
|
+
label: "NEXO CLI",
|
|
21
|
+
});
|
|
22
|
+
process.exit(bridged?.status ?? 1);
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
function resolveNexoHome(rawValue) {
|
|
16
26
|
const homeDir = os.homedir();
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.3",
|
|
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",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"files": [
|
|
74
74
|
"bin/nexo-brain.js",
|
|
75
75
|
"bin/nexo.js",
|
|
76
|
+
"bin/windows-wsl-bridge.js",
|
|
76
77
|
"bin/postinstall.js",
|
|
77
78
|
"scripts/sync_release_artifacts.py",
|
|
78
79
|
"src/",
|
package/src/agent_runner.py
CHANGED
|
@@ -27,7 +27,6 @@ from client_preferences import (
|
|
|
27
27
|
load_client_preferences,
|
|
28
28
|
normalize_client_key,
|
|
29
29
|
resolve_automation_backend,
|
|
30
|
-
resolve_automation_task_profile,
|
|
31
30
|
resolve_client_runtime_profile,
|
|
32
31
|
resolve_terminal_client,
|
|
33
32
|
)
|
|
@@ -936,14 +935,6 @@ def run_automation_prompt(
|
|
|
936
935
|
selected_backend = backend or resolve_automation_backend(preferences=prefs)
|
|
937
936
|
if selected_backend == BACKEND_NONE:
|
|
938
937
|
raise AutomationBackendUnavailableError("Automation backend is disabled in config.")
|
|
939
|
-
|
|
940
|
-
if task_profile:
|
|
941
|
-
profile = resolve_automation_task_profile(task_profile, preferences=prefs)
|
|
942
|
-
selected_backend = profile["backend"] or selected_backend
|
|
943
|
-
if not model:
|
|
944
|
-
model = profile["model"]
|
|
945
|
-
if not reasoning_effort:
|
|
946
|
-
reasoning_effort = profile["reasoning_effort"]
|
|
947
938
|
selected_backend = _resolve_available_backend(selected_backend, preferences=prefs)
|
|
948
939
|
|
|
949
940
|
# Resonance map decides (model, effort) for every call. ``caller`` is
|
package/src/auto_update.py
CHANGED
|
@@ -1630,6 +1630,47 @@ def _migrate_effort_to_resonance(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1630
1630
|
return actions
|
|
1631
1631
|
|
|
1632
1632
|
|
|
1633
|
+
def _cleanup_legacy_email_routing_config(dest: Path = NEXO_HOME) -> list[str]:
|
|
1634
|
+
"""Remove retired email routing overrides from persisted runtime config.
|
|
1635
|
+
|
|
1636
|
+
Older runtimes stored ``automation_task_profile`` inside
|
|
1637
|
+
``runtime/nexo-email/config.json`` (and earlier under ``nexo-email/``).
|
|
1638
|
+
The email monitor now resolves model/effort exclusively from its caller in
|
|
1639
|
+
``resonance_map.py``, so keeping that key around only preserves a stale,
|
|
1640
|
+
misleading second source of truth.
|
|
1641
|
+
"""
|
|
1642
|
+
import json as _json
|
|
1643
|
+
|
|
1644
|
+
actions: list[str] = []
|
|
1645
|
+
candidates = [
|
|
1646
|
+
dest / "runtime" / "nexo-email" / "config.json",
|
|
1647
|
+
dest / "nexo-email" / "config.json",
|
|
1648
|
+
]
|
|
1649
|
+
for path in candidates:
|
|
1650
|
+
if not path.is_file():
|
|
1651
|
+
continue
|
|
1652
|
+
try:
|
|
1653
|
+
payload = _json.loads(path.read_text())
|
|
1654
|
+
except Exception as exc:
|
|
1655
|
+
actions.append(
|
|
1656
|
+
f"email-routing-cleanup-warning:{path.name}:{exc.__class__.__name__}"
|
|
1657
|
+
)
|
|
1658
|
+
continue
|
|
1659
|
+
if not isinstance(payload, dict):
|
|
1660
|
+
continue
|
|
1661
|
+
if "automation_task_profile" not in payload:
|
|
1662
|
+
continue
|
|
1663
|
+
payload.pop("automation_task_profile", None)
|
|
1664
|
+
try:
|
|
1665
|
+
path.write_text(_json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
1666
|
+
actions.append(f"email-routing-cleanup:{path}")
|
|
1667
|
+
except Exception as exc:
|
|
1668
|
+
actions.append(
|
|
1669
|
+
f"email-routing-cleanup-warning:{path.name}:{exc.__class__.__name__}"
|
|
1670
|
+
)
|
|
1671
|
+
return actions
|
|
1672
|
+
|
|
1673
|
+
|
|
1633
1674
|
def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
1634
1675
|
"""Ensure ``resonance_tiers.json`` lives at the public contract path
|
|
1635
1676
|
``NEXO_HOME/personal/brain/resonance_tiers.json`` and purge the legacy
|
|
@@ -4688,6 +4729,14 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4688
4729
|
except Exception as exc:
|
|
4689
4730
|
actions.append(f"v6-purge-warning:{exc.__class__.__name__}")
|
|
4690
4731
|
|
|
4732
|
+
try:
|
|
4733
|
+
_emit_progress(progress_fn, "Cleaning legacy email routing overrides...")
|
|
4734
|
+
email_actions = _cleanup_legacy_email_routing_config(dest)
|
|
4735
|
+
for action in email_actions:
|
|
4736
|
+
actions.append(action)
|
|
4737
|
+
except Exception as exc:
|
|
4738
|
+
actions.append(f"email-routing-cleanup-warning:{exc.__class__.__name__}")
|
|
4739
|
+
|
|
4691
4740
|
try:
|
|
4692
4741
|
_emit_progress(progress_fn, "Ensuring local classifier baseline...")
|
|
4693
4742
|
if _is_ephemeral_runtime_install(dest):
|
package/src/cli.py
CHANGED
|
@@ -2518,6 +2518,7 @@ def _skills_compose(args):
|
|
|
2518
2518
|
def _uninstall(args):
|
|
2519
2519
|
"""Stop all crons, remove MCP config and hooks, preserve user data."""
|
|
2520
2520
|
from pathlib import Path
|
|
2521
|
+
from windows_runtime import cleanup_windows_host_artifacts, running_inside_wsl, running_from_windows_host
|
|
2521
2522
|
|
|
2522
2523
|
nexo_home = Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo"))
|
|
2523
2524
|
dry_run = args.dry_run
|
|
@@ -2566,6 +2567,20 @@ def _uninstall(args):
|
|
|
2566
2567
|
if not dry_run:
|
|
2567
2568
|
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
2568
2569
|
|
|
2570
|
+
if running_inside_wsl() or running_from_windows_host():
|
|
2571
|
+
host_cleanup = cleanup_windows_host_artifacts(
|
|
2572
|
+
delete_data=delete_data,
|
|
2573
|
+
dry_run=dry_run,
|
|
2574
|
+
)
|
|
2575
|
+
for action in host_cleanup.get("actions", []):
|
|
2576
|
+
log_action(
|
|
2577
|
+
action.get("category", "remove-win-host-artifact"),
|
|
2578
|
+
action.get("detail", ""),
|
|
2579
|
+
action.get("path", ""),
|
|
2580
|
+
)
|
|
2581
|
+
for error in host_cleanup.get("errors", []):
|
|
2582
|
+
errors.append(f"Windows host cleanup: {error}")
|
|
2583
|
+
|
|
2569
2584
|
# ── 2. Remove MCP server and hooks from Claude Code settings ──
|
|
2570
2585
|
claude_settings = Path.home() / ".claude" / "settings.json"
|
|
2571
2586
|
if claude_settings.exists():
|