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.14",
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>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</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
- ["script-template.py", "nexo_helper.py", "skill-template.md", "skill-script-template.py"].forEach((f) => {
1768
+ for (const f of fs.readdirSync(templatesSrc)) {
1721
1769
  const src = path.join(templatesSrc, f);
1722
- if (fs.existsSync(src)) {
1723
- fs.copyFileSync(src, path.join(templatesDest, f));
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
- ["script-template.py", "nexo_helper.py", "skill-template.md", "skill-script-template.py"].forEach(f => {
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
- if (fs.existsSync(src)) {
2392
- fs.copyFileSync(src, path.join(templatesDest, f));
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(" Script and skill templates installed.");
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.14",
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 shutil.which("claude") or ""
481
- codex_bin = os.environ.get("CODEX_BIN", "").strip() or shutil.which("codex") 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