nexo-brain 5.3.14 → 5.3.16
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": "5.3.
|
|
3
|
+
"version": "5.3.16",
|
|
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/bin/nexo-brain.js
CHANGED
|
@@ -177,6 +177,54 @@ function getCoreRuntimePackages() {
|
|
|
177
177
|
return ["db", "cognitive", "doctor"];
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
function resolveLaunchAgentPath(home) {
|
|
181
|
+
const parts = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin",
|
|
182
|
+
path.join(home, ".local/bin"), path.join(home, ".nexo/bin")];
|
|
183
|
+
// Detect nvm node
|
|
184
|
+
const nvmDir = path.join(home, ".nvm/versions/node");
|
|
185
|
+
try {
|
|
186
|
+
const versions = fs.readdirSync(nvmDir)
|
|
187
|
+
.map(v => ({ name: v, mtime: fs.statSync(path.join(nvmDir, v)).mtimeMs }))
|
|
188
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
189
|
+
for (const v of versions) {
|
|
190
|
+
const nodeBin = path.join(nvmDir, v.name, "bin");
|
|
191
|
+
if (fs.existsSync(path.join(nodeBin, "node"))) {
|
|
192
|
+
parts.unshift(nodeBin);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch { /* nvm not installed — skip */ }
|
|
197
|
+
return parts.join(":");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function setupKeychainPassFile(nexoHome) {
|
|
201
|
+
if (process.platform !== "darwin") return;
|
|
202
|
+
const configDir = path.join(nexoHome, "config");
|
|
203
|
+
const passFile = path.join(configDir, ".keychain-pass");
|
|
204
|
+
if (fs.existsSync(passFile)) return; // already set up
|
|
205
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
206
|
+
log("");
|
|
207
|
+
log("macOS Keychain setup for headless automation:");
|
|
208
|
+
log(" Claude Code stores auth in the login Keychain, which auto-locks.");
|
|
209
|
+
log(" Background jobs need to unlock it. Enter your macOS login password");
|
|
210
|
+
log(" (stored locally in ~/.nexo/config/.keychain-pass, chmod 600).");
|
|
211
|
+
log("");
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
const rl = require("readline").createInterface({ input: process.stdin, output: process.stdout });
|
|
214
|
+
rl.question(" macOS login password (or Enter to skip): ", (answer) => {
|
|
215
|
+
rl.close();
|
|
216
|
+
if (answer && answer.trim()) {
|
|
217
|
+
fs.writeFileSync(passFile, answer.trim(), { mode: 0o600 });
|
|
218
|
+
log(" Keychain password saved. Background jobs will auto-unlock.");
|
|
219
|
+
} else {
|
|
220
|
+
log(" Skipped. Background jobs may fail with 'Not logged in' if Keychain locks.");
|
|
221
|
+
log(" Run: echo 'YOUR_PASSWORD' > ~/.nexo/config/.keychain-pass && chmod 600 ~/.nexo/config/.keychain-pass");
|
|
222
|
+
}
|
|
223
|
+
resolve();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
180
228
|
function isProtectedMacPath(candidate) {
|
|
181
229
|
if (process.platform !== "darwin" || !candidate) return false;
|
|
182
230
|
const homeDir = require("os").homedir();
|
|
@@ -1250,7 +1298,7 @@ function installAllProcesses(platform, pythonPath, nexoHome, schedule, launchAge
|
|
|
1250
1298
|
<key>NEXO_CODE</key>
|
|
1251
1299
|
<string>${nexoCode}</string>
|
|
1252
1300
|
<key>PATH</key>
|
|
1253
|
-
<string
|
|
1301
|
+
<string>${resolveLaunchAgentPath(home)}</string>
|
|
1254
1302
|
</dict>
|
|
1255
1303
|
</dict>
|
|
1256
1304
|
</plist>`;
|
|
@@ -1717,12 +1765,21 @@ async function main() {
|
|
|
1717
1765
|
const templatesDest = path.join(NEXO_HOME, "templates");
|
|
1718
1766
|
if (fs.existsSync(templatesSrc)) {
|
|
1719
1767
|
fs.mkdirSync(templatesDest, { recursive: true });
|
|
1720
|
-
|
|
1768
|
+
for (const f of fs.readdirSync(templatesSrc)) {
|
|
1721
1769
|
const src = path.join(templatesSrc, f);
|
|
1722
|
-
|
|
1723
|
-
|
|
1770
|
+
const dest = path.join(templatesDest, f);
|
|
1771
|
+
if (fs.statSync(src).isFile()) {
|
|
1772
|
+
fs.copyFileSync(src, dest);
|
|
1773
|
+
} else if (fs.statSync(src).isDirectory()) {
|
|
1774
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
1775
|
+
for (const sf of fs.readdirSync(src)) {
|
|
1776
|
+
const ssrc = path.join(src, sf);
|
|
1777
|
+
if (fs.statSync(ssrc).isFile()) {
|
|
1778
|
+
fs.copyFileSync(ssrc, path.join(dest, sf));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1724
1781
|
}
|
|
1725
|
-
}
|
|
1782
|
+
}
|
|
1726
1783
|
}
|
|
1727
1784
|
|
|
1728
1785
|
logMacPermissionsNotice(NEXO_HOME, syncPython);
|
|
@@ -2386,13 +2443,23 @@ async function main() {
|
|
|
2386
2443
|
const templatesDest = path.join(NEXO_HOME, "templates");
|
|
2387
2444
|
fs.mkdirSync(templatesDest, { recursive: true });
|
|
2388
2445
|
if (fs.existsSync(templateDir)) {
|
|
2389
|
-
|
|
2446
|
+
// Copy all template files (not just a hardcoded subset)
|
|
2447
|
+
for (const f of fs.readdirSync(templateDir)) {
|
|
2390
2448
|
const src = path.join(templateDir, f);
|
|
2391
|
-
|
|
2392
|
-
|
|
2449
|
+
const dest = path.join(templatesDest, f);
|
|
2450
|
+
if (fs.statSync(src).isFile()) {
|
|
2451
|
+
fs.copyFileSync(src, dest);
|
|
2452
|
+
} else if (fs.statSync(src).isDirectory()) {
|
|
2453
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
2454
|
+
for (const sf of fs.readdirSync(src)) {
|
|
2455
|
+
const ssrc = path.join(src, sf);
|
|
2456
|
+
if (fs.statSync(ssrc).isFile()) {
|
|
2457
|
+
fs.copyFileSync(ssrc, path.join(dest, sf));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2393
2460
|
}
|
|
2394
|
-
}
|
|
2395
|
-
log("
|
|
2461
|
+
}
|
|
2462
|
+
log(" All templates installed.");
|
|
2396
2463
|
}
|
|
2397
2464
|
|
|
2398
2465
|
// Hooks directory
|
|
@@ -2965,6 +3032,9 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
2965
3032
|
// Note: prevent-sleep and tcc-approve are now part of ALL_PROCESSES
|
|
2966
3033
|
// and installed by installAllProcesses() above. No separate caffeinate block needed.
|
|
2967
3034
|
|
|
3035
|
+
// Step 7b: macOS Keychain setup for headless automation
|
|
3036
|
+
await setupKeychainPassFile(NEXO_HOME);
|
|
3037
|
+
|
|
2968
3038
|
// Step 8: Create shell alias and add runtime CLI to PATH
|
|
2969
3039
|
log("Creating shell alias...");
|
|
2970
3040
|
const aliasName = operatorName.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.16",
|
|
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",
|
|
@@ -474,11 +474,35 @@ def _claude_desktop_config_path(home: Path) -> Path:
|
|
|
474
474
|
return home / ".config" / "Claude" / "claude_desktop_config.json"
|
|
475
475
|
|
|
476
476
|
|
|
477
|
+
def _which_with_nvm(name: str, home: Path | None = None) -> str:
|
|
478
|
+
"""Like shutil.which but also searches nvm and ~/.nexo/bin."""
|
|
479
|
+
found = shutil.which(name)
|
|
480
|
+
if found:
|
|
481
|
+
return found
|
|
482
|
+
home = home or _user_home()
|
|
483
|
+
# Check ~/.nexo/bin
|
|
484
|
+
candidate = home / ".nexo" / "bin" / name
|
|
485
|
+
if candidate.exists():
|
|
486
|
+
return str(candidate)
|
|
487
|
+
# Check nvm node bins
|
|
488
|
+
nvm_dir = home / ".nvm" / "versions" / "node"
|
|
489
|
+
if nvm_dir.is_dir():
|
|
490
|
+
try:
|
|
491
|
+
versions = sorted(nvm_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
492
|
+
for v in versions:
|
|
493
|
+
candidate = v / "bin" / name
|
|
494
|
+
if candidate.exists():
|
|
495
|
+
return str(candidate)
|
|
496
|
+
except OSError:
|
|
497
|
+
pass
|
|
498
|
+
return ""
|
|
499
|
+
|
|
500
|
+
|
|
477
501
|
def detect_installed_clients(user_home: str | os.PathLike[str] | None = None) -> dict[str, dict]:
|
|
478
502
|
home = Path(user_home).expanduser() if user_home else _user_home()
|
|
479
503
|
|
|
480
|
-
claude_bin = os.environ.get("CLAUDE_BIN", "").strip() or
|
|
481
|
-
codex_bin = os.environ.get("CODEX_BIN", "").strip() or
|
|
504
|
+
claude_bin = os.environ.get("CLAUDE_BIN", "").strip() or _which_with_nvm("claude", home)
|
|
505
|
+
codex_bin = os.environ.get("CODEX_BIN", "").strip() or _which_with_nvm("codex", home)
|
|
482
506
|
|
|
483
507
|
if sys.platform == "darwin":
|
|
484
508
|
desktop_app = next(
|
|
@@ -14,6 +14,13 @@ shift
|
|
|
14
14
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
15
15
|
DB="$NEXO_HOME/data/nexo.db"
|
|
16
16
|
|
|
17
|
+
# Unlock macOS Keychain so headless Claude Code can read auth tokens.
|
|
18
|
+
# Claude Code stores its API key in the login keychain which auto-locks.
|
|
19
|
+
KEYCHAIN_PASS_FILE="$NEXO_HOME/config/.keychain-pass"
|
|
20
|
+
if [ -f "$KEYCHAIN_PASS_FILE" ] && [ "$(uname)" = "Darwin" ]; then
|
|
21
|
+
security unlock-keychain -p "$(cat "$KEYCHAIN_PASS_FILE")" ~/Library/Keychains/login.keychain-db 2>/dev/null || true
|
|
22
|
+
fi
|
|
23
|
+
|
|
17
24
|
# Record start
|
|
18
25
|
RUN_ID=$(sqlite3 "$DB" "INSERT INTO cron_runs (cron_id) VALUES ('$CRON_ID'); SELECT last_insert_rowid();" 2>/dev/null)
|
|
19
26
|
|