nexo-brain 2.6.11 → 2.6.13
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 +22 -12
- package/bin/nexo-brain.js +483 -56
- package/package.json +4 -1
- package/src/agent_runner.py +322 -0
- package/src/auto_update.py +12 -3
- package/src/cli.py +22 -10
- package/src/client_preferences.py +394 -0
- package/src/client_sync.py +78 -0
- package/src/cron_recovery.py +8 -1
- package/src/crons/manifest.json +6 -0
- package/src/crons/sync.py +14 -1
- package/src/doctor/providers/runtime.py +109 -1
- package/src/plugins/schedule.py +69 -12
- package/src/plugins/update.py +5 -1
- package/src/runtime_power.py +23 -0
- package/src/script_registry.py +62 -1
- package/src/scripts/check-context.py +102 -100
- package/src/scripts/deep-sleep/extract.py +29 -54
- package/src/scripts/deep-sleep/synthesize.py +14 -38
- package/src/scripts/nexo-agent-run.py +73 -0
- package/src/scripts/nexo-catchup.py +15 -19
- package/src/scripts/nexo-daily-self-audit.py +17 -14
- package/src/scripts/nexo-evolution-run.py +25 -55
- package/src/scripts/nexo-immune.py +17 -15
- package/src/scripts/nexo-learning-validator.py +90 -58
- package/src/scripts/nexo-postmortem-consolidator.py +15 -14
- package/src/scripts/nexo-sleep.py +20 -14
- package/src/scripts/nexo-synthesis.py +19 -12
- package/src/scripts/nexo-update.sh +28 -2
- package/src/scripts/nexo-watchdog.sh +34 -10
- package/templates/nexo_helper.py +45 -0
- package/templates/plugin-template.py +4 -0
- package/templates/script-template.py +13 -2
- package/templates/skill-script-template.py +8 -0
package/bin/nexo-brain.js
CHANGED
|
@@ -103,6 +103,8 @@ function getCoreRuntimeFlatFiles() {
|
|
|
103
103
|
"migrate_embeddings.py",
|
|
104
104
|
"auto_close_sessions.py",
|
|
105
105
|
"client_sync.py",
|
|
106
|
+
"client_preferences.py",
|
|
107
|
+
"agent_runner.py",
|
|
106
108
|
"auto_update.py",
|
|
107
109
|
"tools_sessions.py",
|
|
108
110
|
"tools_coordination.py",
|
|
@@ -308,7 +310,7 @@ const ALL_PROCESSES = [
|
|
|
308
310
|
type: "interval", intervalMinutes: 30, purpose: "System immunity checks" },
|
|
309
311
|
// --- Every 2 hours ---
|
|
310
312
|
{ name: "synthesis", script: "nexo-synthesis.py", interpreter: "python", scriptDir: "scripts",
|
|
311
|
-
type: "interval", intervalMinutes: 120, purpose: "Memory synthesis" },
|
|
313
|
+
type: "interval", intervalMinutes: 120, optional: "automation", purpose: "Memory synthesis" },
|
|
312
314
|
// --- Every hour ---
|
|
313
315
|
{ name: "backup", script: "nexo-backup.sh", interpreter: "bash", scriptDir: "scripts",
|
|
314
316
|
type: "interval", intervalMinutes: 60, purpose: "DB backups" },
|
|
@@ -327,16 +329,16 @@ const ALL_PROCESSES = [
|
|
|
327
329
|
{ name: "cognitive-decay", script: "nexo-cognitive-decay.py", interpreter: "python", scriptDir: "scripts",
|
|
328
330
|
type: "daily", defaultHour: 3, defaultMinute: 0, purpose: "Memory decay" },
|
|
329
331
|
{ name: "postmortem", script: "nexo-postmortem-consolidator.py", interpreter: "python", scriptDir: "scripts",
|
|
330
|
-
type: "daily", defaultHour: 23, defaultMinute: 30, purpose: "Session consolidation" },
|
|
332
|
+
type: "daily", defaultHour: 23, defaultMinute: 30, optional: "automation", purpose: "Session consolidation" },
|
|
331
333
|
{ name: "self-audit", script: "nexo-daily-self-audit.py", interpreter: "python", scriptDir: "scripts",
|
|
332
|
-
type: "daily", defaultHour: 7, defaultMinute: 0, purpose: "Self-diagnostic" },
|
|
334
|
+
type: "daily", defaultHour: 7, defaultMinute: 0, optional: "automation", purpose: "Self-diagnostic" },
|
|
333
335
|
{ name: "sleep", script: "nexo-sleep.py", interpreter: "python", scriptDir: "scripts",
|
|
334
|
-
type: "daily", defaultHour: 4, defaultMinute: 0, purpose: "Sleep cycle" },
|
|
336
|
+
type: "daily", defaultHour: 4, defaultMinute: 0, optional: "automation", purpose: "Sleep cycle" },
|
|
335
337
|
{ name: "deep-sleep", script: "nexo-deep-sleep.sh", interpreter: "bash", scriptDir: "scripts",
|
|
336
|
-
type: "daily", defaultHour: 4, defaultMinute: 30, purpose: "Deep sleep analysis" },
|
|
338
|
+
type: "daily", defaultHour: 4, defaultMinute: 30, optional: "automation", purpose: "Deep sleep analysis" },
|
|
337
339
|
// --- Weekly (day + time from schedule.json) ---
|
|
338
340
|
{ name: "evolution", script: "nexo-evolution-run.py", interpreter: "python", scriptDir: "scripts",
|
|
339
|
-
type: "weekly", defaultDay: "sunday", defaultHour: 3, defaultMinute: 0, purpose: "Self-evolution" },
|
|
341
|
+
type: "weekly", defaultDay: "sunday", defaultHour: 3, defaultMinute: 0, optional: "automation", purpose: "Self-evolution" },
|
|
340
342
|
{ name: "followup-hygiene", script: "nexo-followup-hygiene.py", interpreter: "python", scriptDir: "scripts",
|
|
341
343
|
type: "weekly", defaultDay: "sunday", defaultHour: 5, defaultMinute: 0, purpose: "Cleanup stale followups" },
|
|
342
344
|
];
|
|
@@ -467,6 +469,29 @@ function getDefaultSchedule(timezone) {
|
|
|
467
469
|
return {
|
|
468
470
|
timezone: timezone || "UTC",
|
|
469
471
|
auto_update: true,
|
|
472
|
+
interactive_clients: {
|
|
473
|
+
claude_code: true,
|
|
474
|
+
codex: false,
|
|
475
|
+
claude_desktop: false,
|
|
476
|
+
},
|
|
477
|
+
default_terminal_client: "claude_code",
|
|
478
|
+
automation_enabled: true,
|
|
479
|
+
automation_backend: "claude_code",
|
|
480
|
+
client_runtime_profiles: {
|
|
481
|
+
claude_code: {
|
|
482
|
+
model: "opus",
|
|
483
|
+
reasoning_effort: "",
|
|
484
|
+
},
|
|
485
|
+
codex: {
|
|
486
|
+
model: "gpt-5.4",
|
|
487
|
+
reasoning_effort: "xhigh",
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
client_install_preferences: {
|
|
491
|
+
claude_code: "ask",
|
|
492
|
+
codex: "ask",
|
|
493
|
+
claude_desktop: "manual",
|
|
494
|
+
},
|
|
470
495
|
power_policy: "unset",
|
|
471
496
|
power_policy_version: 2,
|
|
472
497
|
full_disk_access_status: "unset",
|
|
@@ -517,6 +542,409 @@ function normalizePublicContributionConfig(config = {}) {
|
|
|
517
542
|
return merged;
|
|
518
543
|
}
|
|
519
544
|
|
|
545
|
+
function detectInstalledClients() {
|
|
546
|
+
const homeDir = require("os").homedir();
|
|
547
|
+
const desktopConfig = process.platform === "darwin"
|
|
548
|
+
? path.join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
549
|
+
: process.platform === "win32"
|
|
550
|
+
? path.join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json")
|
|
551
|
+
: path.join(homeDir, ".config", "Claude", "claude_desktop_config.json");
|
|
552
|
+
const desktopApps = process.platform === "darwin"
|
|
553
|
+
? [path.join(homeDir, "Applications", "Claude.app"), "/Applications/Claude.app"]
|
|
554
|
+
: [];
|
|
555
|
+
const desktopAppPath = desktopApps.find((candidate) => fs.existsSync(candidate)) || "";
|
|
556
|
+
const claudeBin = run("which claude") || "";
|
|
557
|
+
const codexBin = run("which codex") || "";
|
|
558
|
+
return {
|
|
559
|
+
claude_code: {
|
|
560
|
+
installed: Boolean(claudeBin),
|
|
561
|
+
path: claudeBin,
|
|
562
|
+
detectedBy: claudeBin ? "binary" : "missing",
|
|
563
|
+
},
|
|
564
|
+
codex: {
|
|
565
|
+
installed: Boolean(codexBin),
|
|
566
|
+
path: codexBin,
|
|
567
|
+
detectedBy: codexBin ? "binary" : "missing",
|
|
568
|
+
},
|
|
569
|
+
claude_desktop: {
|
|
570
|
+
installed: Boolean(desktopAppPath || fs.existsSync(desktopConfig)),
|
|
571
|
+
path: desktopAppPath || desktopConfig,
|
|
572
|
+
detectedBy: desktopAppPath ? "app" : (fs.existsSync(desktopConfig) ? "config" : "missing"),
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function clientSetupStrings(lang) {
|
|
578
|
+
if (lang === "es") {
|
|
579
|
+
return {
|
|
580
|
+
title: "Shared brain siempre activo. Ahora elige clientes y backend de automatización.",
|
|
581
|
+
detected: "Clientes detectados",
|
|
582
|
+
yes: "sí",
|
|
583
|
+
no: "no",
|
|
584
|
+
useClaudeCodeQ: " ¿Quieres usar Claude Code como cliente interactivo? (recomendado)",
|
|
585
|
+
useCodexQ: " ¿Quieres usar Codex como cliente interactivo?",
|
|
586
|
+
useDesktopQ: " ¿Quieres conectar Claude Desktop al mismo brain?",
|
|
587
|
+
defaultTerminalQ: " ¿Qué cliente debe abrir `nexo chat` por defecto?",
|
|
588
|
+
automationQ: " ¿Quieres automatización en background? (sleep, deep-sleep, synthesis, self-audit, evolution, postmortem)",
|
|
589
|
+
automationBackendQ: " ¿Qué backend debe ejecutar esa automatización?",
|
|
590
|
+
installClaudeQ: " Claude Code no está instalado. ¿Quieres instalarlo ahora?",
|
|
591
|
+
installCodexQ: " Codex no está instalado. ¿Quieres instalarlo ahora?",
|
|
592
|
+
installingClaude: "Instalando Claude Code...",
|
|
593
|
+
installingCodex: "Instalando Codex...",
|
|
594
|
+
desktopManual: "Claude Desktop no se instala desde NEXO. Cuando exista, se conectará con la sync de clientes.",
|
|
595
|
+
terminalFallback: (label) => `El cliente terminal elegido no está disponible. \`nexo chat\` quedará pendiente hasta instalar ${label}.`,
|
|
596
|
+
automationDisabled: (label) => `El backend ${label} sigue sin estar disponible. Se desactiva la automatización por ahora.`,
|
|
597
|
+
summary: (defaultClient, defaultProfile, backend, backendProfile, automationEnabled) =>
|
|
598
|
+
`Configuración clientes: chat=${defaultClient}(${defaultProfile}), automation=${automationEnabled ? `${backend}(${backendProfile})` : "none"}`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
title: "Shared brain is always on. Now choose your clients and automation backend.",
|
|
603
|
+
detected: "Detected clients",
|
|
604
|
+
yes: "yes",
|
|
605
|
+
no: "no",
|
|
606
|
+
useClaudeCodeQ: " Use Claude Code as an interactive client? (recommended)",
|
|
607
|
+
useCodexQ: " Use Codex as an interactive client?",
|
|
608
|
+
useDesktopQ: " Connect Claude Desktop to the same brain?",
|
|
609
|
+
defaultTerminalQ: " Which client should `nexo chat` open by default?",
|
|
610
|
+
automationQ: " Enable background automation? (sleep, deep-sleep, synthesis, self-audit, evolution, postmortem)",
|
|
611
|
+
automationBackendQ: " Which backend should run that automation?",
|
|
612
|
+
installClaudeQ: " Claude Code is not installed. Install it now?",
|
|
613
|
+
installCodexQ: " Codex is not installed. Install it now?",
|
|
614
|
+
installingClaude: "Installing Claude Code...",
|
|
615
|
+
installingCodex: "Installing Codex...",
|
|
616
|
+
desktopManual: "Claude Desktop is not installed by NEXO. When it appears, client sync will connect it.",
|
|
617
|
+
terminalFallback: (label) => `The selected terminal client is still unavailable. \`nexo chat\` will stay pending until ${label} is installed.`,
|
|
618
|
+
automationDisabled: (label) => `${label} is still unavailable. Disabling background automation for now.`,
|
|
619
|
+
summary: (defaultClient, defaultProfile, backend, backendProfile, automationEnabled) =>
|
|
620
|
+
`Client setup: chat=${defaultClient}(${defaultProfile}), automation=${automationEnabled ? `${backend}(${backendProfile})` : "none"}`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function _yn(answer, defaultValue) {
|
|
625
|
+
const value = String(answer || "").trim().toLowerCase();
|
|
626
|
+
if (!value) return defaultValue;
|
|
627
|
+
if (["y", "yes", "s", "si", "sí", "1"].includes(value)) return true;
|
|
628
|
+
if (["n", "no", "0"].includes(value)) return false;
|
|
629
|
+
return defaultValue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function askYesNo(question, defaultValue) {
|
|
633
|
+
const suffix = defaultValue ? " [Y/n]: " : " [y/N]: ";
|
|
634
|
+
const answer = await ask(question + suffix);
|
|
635
|
+
return _yn(answer, defaultValue);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function askChoice(question, options, defaultValue) {
|
|
639
|
+
let prompt = `${question}\n`;
|
|
640
|
+
options.forEach((option, idx) => {
|
|
641
|
+
const marker = option.value === defaultValue ? " (default)" : "";
|
|
642
|
+
prompt += ` ${idx + 1}. ${option.label}${marker}\n`;
|
|
643
|
+
});
|
|
644
|
+
prompt += " > ";
|
|
645
|
+
const answer = (await ask(prompt)).trim().toLowerCase();
|
|
646
|
+
if (!answer) return defaultValue;
|
|
647
|
+
const asIndex = parseInt(answer, 10);
|
|
648
|
+
if (!Number.isNaN(asIndex) && asIndex >= 1 && asIndex <= options.length) {
|
|
649
|
+
return options[asIndex - 1].value;
|
|
650
|
+
}
|
|
651
|
+
const byValue = options.find((option) => option.value === answer);
|
|
652
|
+
return byValue ? byValue.value : defaultValue;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function defaultClientRuntimeProfiles() {
|
|
656
|
+
return {
|
|
657
|
+
claude_code: {
|
|
658
|
+
model: "opus",
|
|
659
|
+
reasoning_effort: "",
|
|
660
|
+
},
|
|
661
|
+
codex: {
|
|
662
|
+
model: "gpt-5.4",
|
|
663
|
+
reasoning_effort: "xhigh",
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function runtimeClientLabel(client) {
|
|
669
|
+
if (client === "claude_code") return "Claude Code";
|
|
670
|
+
if (client === "codex") return "Codex";
|
|
671
|
+
return client;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function formatRuntimeProfile(profile = {}) {
|
|
675
|
+
const model = String(profile.model || "").trim();
|
|
676
|
+
const effort = String(profile.reasoning_effort || "").trim();
|
|
677
|
+
return effort ? `${model}/${effort}` : model;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function runtimeProfileCatalog(lang, client) {
|
|
681
|
+
const recommended = lang === "es" ? " (recomendado)" : " (recommended)";
|
|
682
|
+
if (client === "claude_code") {
|
|
683
|
+
return {
|
|
684
|
+
modelQuestion: ` ¿Qué modelo debe usar ${runtimeClientLabel(client)} para chat y background cuando sea el cliente/backend activo?`,
|
|
685
|
+
modelQuestionEn: ` Which model should ${runtimeClientLabel(client)} use for chat and background when it is the active client/backend?`,
|
|
686
|
+
effortQuestion: ` ¿Qué nivel de esfuerzo debe usar ${runtimeClientLabel(client)}?`,
|
|
687
|
+
effortQuestionEn: ` Which effort level should ${runtimeClientLabel(client)} use?`,
|
|
688
|
+
customModelQuestion: ` Escribe el alias/nombre de modelo para ${runtimeClientLabel(client)} > `,
|
|
689
|
+
customModelQuestionEn: ` Enter the model alias/name for ${runtimeClientLabel(client)} > `,
|
|
690
|
+
customEffortQuestion: ` Escribe el effort para ${runtimeClientLabel(client)} (vacío = default) > `,
|
|
691
|
+
customEffortQuestionEn: ` Enter the effort for ${runtimeClientLabel(client)} (blank = default) > `,
|
|
692
|
+
modelDefault: "opus",
|
|
693
|
+
effortDefault: "",
|
|
694
|
+
modelOptions: [
|
|
695
|
+
{ value: "opus", label: `Opus latest${recommended}` },
|
|
696
|
+
{ value: "sonnet", label: "Sonnet latest" },
|
|
697
|
+
{ value: "custom", label: lang === "es" ? "Modelo personalizado" : "Custom model" },
|
|
698
|
+
],
|
|
699
|
+
effortOptions: [
|
|
700
|
+
{ value: "", label: lang === "es" ? `Effort por defecto${recommended}` : `Default effort${recommended}` },
|
|
701
|
+
{ value: "high", label: "high" },
|
|
702
|
+
{ value: "max", label: "max" },
|
|
703
|
+
{ value: "custom", label: lang === "es" ? "Effort personalizado" : "Custom effort" },
|
|
704
|
+
],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
modelQuestion: ` ¿Qué modelo debe usar ${runtimeClientLabel(client)} para chat y background cuando sea el cliente/backend activo?`,
|
|
710
|
+
modelQuestionEn: ` Which model should ${runtimeClientLabel(client)} use for chat and background when it is the active client/backend?`,
|
|
711
|
+
effortQuestion: ` ¿Qué razonamiento debe usar ${runtimeClientLabel(client)}?`,
|
|
712
|
+
effortQuestionEn: ` Which reasoning effort should ${runtimeClientLabel(client)} use?`,
|
|
713
|
+
customModelQuestion: ` Escribe el nombre del modelo para ${runtimeClientLabel(client)} > `,
|
|
714
|
+
customModelQuestionEn: ` Enter the model name for ${runtimeClientLabel(client)} > `,
|
|
715
|
+
customEffortQuestion: ` Escribe el reasoning effort para ${runtimeClientLabel(client)} > `,
|
|
716
|
+
customEffortQuestionEn: ` Enter the reasoning effort for ${runtimeClientLabel(client)} > `,
|
|
717
|
+
modelDefault: "gpt-5.4",
|
|
718
|
+
effortDefault: "xhigh",
|
|
719
|
+
modelOptions: [
|
|
720
|
+
{ value: "gpt-5.4", label: `GPT-5.4${recommended}` },
|
|
721
|
+
{ value: "gpt-5.4-pro", label: "GPT-5.4 Pro" },
|
|
722
|
+
{ value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
|
|
723
|
+
{ value: "custom", label: lang === "es" ? "Modelo personalizado" : "Custom model" },
|
|
724
|
+
],
|
|
725
|
+
effortOptions: [
|
|
726
|
+
{ value: "xhigh", label: `xhigh${recommended}` },
|
|
727
|
+
{ value: "high", label: "high" },
|
|
728
|
+
{ value: "medium", label: "medium" },
|
|
729
|
+
{ value: "low", label: "low" },
|
|
730
|
+
{ value: "none", label: "none" },
|
|
731
|
+
{ value: "custom", label: lang === "es" ? "Effort personalizado" : "Custom effort" },
|
|
732
|
+
],
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function askClientRuntimeProfile({ lang, client, currentProfile }) {
|
|
737
|
+
const catalog = runtimeProfileCatalog(lang, client);
|
|
738
|
+
const modelQuestion = lang === "es" ? catalog.modelQuestion : catalog.modelQuestionEn;
|
|
739
|
+
const effortQuestion = lang === "es" ? catalog.effortQuestion : catalog.effortQuestionEn;
|
|
740
|
+
const customModelQuestion = lang === "es" ? catalog.customModelQuestion : catalog.customModelQuestionEn;
|
|
741
|
+
const customEffortQuestion = lang === "es" ? catalog.customEffortQuestion : catalog.customEffortQuestionEn;
|
|
742
|
+
let model = await askChoice(modelQuestion, catalog.modelOptions, currentProfile.model || catalog.modelDefault);
|
|
743
|
+
if (model === "custom") {
|
|
744
|
+
model = (await ask(customModelQuestion)).trim() || catalog.modelDefault;
|
|
745
|
+
}
|
|
746
|
+
let reasoningEffort = await askChoice(
|
|
747
|
+
effortQuestion,
|
|
748
|
+
catalog.effortOptions,
|
|
749
|
+
currentProfile.reasoning_effort ?? catalog.effortDefault,
|
|
750
|
+
);
|
|
751
|
+
if (reasoningEffort === "custom") {
|
|
752
|
+
reasoningEffort = (await ask(customEffortQuestion)).trim();
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
model,
|
|
756
|
+
reasoning_effort: reasoningEffort,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function defaultClientSetup(detected) {
|
|
761
|
+
return {
|
|
762
|
+
interactive_clients: {
|
|
763
|
+
claude_code: true,
|
|
764
|
+
codex: Boolean(detected.codex.installed),
|
|
765
|
+
claude_desktop: Boolean(detected.claude_desktop.installed),
|
|
766
|
+
},
|
|
767
|
+
default_terminal_client: "claude_code",
|
|
768
|
+
automation_enabled: true,
|
|
769
|
+
automation_backend: "claude_code",
|
|
770
|
+
client_runtime_profiles: defaultClientRuntimeProfiles(),
|
|
771
|
+
client_install_preferences: {
|
|
772
|
+
claude_code: "ask",
|
|
773
|
+
codex: "ask",
|
|
774
|
+
claude_desktop: "manual",
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function applyClientSetupToSchedule(schedule, setup) {
|
|
780
|
+
schedule.interactive_clients = {
|
|
781
|
+
claude_code: Boolean(setup.interactive_clients.claude_code),
|
|
782
|
+
codex: Boolean(setup.interactive_clients.codex),
|
|
783
|
+
claude_desktop: Boolean(setup.interactive_clients.claude_desktop),
|
|
784
|
+
};
|
|
785
|
+
schedule.default_terminal_client = setup.default_terminal_client;
|
|
786
|
+
schedule.automation_enabled = Boolean(setup.automation_enabled);
|
|
787
|
+
schedule.automation_backend = schedule.automation_enabled ? setup.automation_backend : "none";
|
|
788
|
+
schedule.client_runtime_profiles = {
|
|
789
|
+
...defaultClientRuntimeProfiles(),
|
|
790
|
+
...(setup.client_runtime_profiles || {}),
|
|
791
|
+
};
|
|
792
|
+
schedule.client_install_preferences = { ...(setup.client_install_preferences || {}) };
|
|
793
|
+
return schedule;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function requiredCliClients(setup) {
|
|
797
|
+
const required = new Set();
|
|
798
|
+
if (setup.interactive_clients.claude_code || setup.default_terminal_client === "claude_code" || setup.automation_backend === "claude_code") {
|
|
799
|
+
required.add("claude_code");
|
|
800
|
+
}
|
|
801
|
+
if (setup.interactive_clients.codex || setup.default_terminal_client === "codex" || setup.automation_backend === "codex") {
|
|
802
|
+
required.add("codex");
|
|
803
|
+
}
|
|
804
|
+
return Array.from(required);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function installClaudeCodeCli(platform) {
|
|
808
|
+
let claudeInstalled = run("which claude");
|
|
809
|
+
if (claudeInstalled) return { installed: true, path: claudeInstalled };
|
|
810
|
+
|
|
811
|
+
spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], { stdio: "pipe", timeout: 60000 });
|
|
812
|
+
claudeInstalled = run("which claude");
|
|
813
|
+
if (!claudeInstalled) {
|
|
814
|
+
const npmCmd = platform === "linux" ? "sudo" : "npm";
|
|
815
|
+
const npmArgs = platform === "linux" ? ["npm", "install", "-g", "@anthropic-ai/claude-code"] : ["install", "-g", "@anthropic-ai/claude-code"];
|
|
816
|
+
spawnSync(npmCmd, npmArgs, { stdio: "inherit" });
|
|
817
|
+
claudeInstalled = run("which claude");
|
|
818
|
+
}
|
|
819
|
+
return { installed: Boolean(claudeInstalled), path: claudeInstalled || "" };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function installCodexCli() {
|
|
823
|
+
const before = run("which codex");
|
|
824
|
+
if (before) return { installed: true, path: before };
|
|
825
|
+
spawnSync("npm", ["install", "-g", "@openai/codex"], { stdio: "inherit" });
|
|
826
|
+
const codexInstalled = run("which codex") || "";
|
|
827
|
+
return { installed: Boolean(codexInstalled), path: codexInstalled };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function configureClientSetup({ lang, useDefaults, autoInstall, detected }) {
|
|
831
|
+
const strings = clientSetupStrings(lang);
|
|
832
|
+
const setup = defaultClientSetup(detected);
|
|
833
|
+
setup.client_install_preferences = {
|
|
834
|
+
claude_code: autoInstall === "auto" ? "auto" : "ask",
|
|
835
|
+
codex: autoInstall === "auto" ? "auto" : "ask",
|
|
836
|
+
claude_desktop: "manual",
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
if (!useDefaults) {
|
|
840
|
+
console.log("");
|
|
841
|
+
log(strings.title);
|
|
842
|
+
log(`${strings.detected}: Claude Code=${detected.claude_code.installed ? strings.yes : strings.no}, Codex=${detected.codex.installed ? strings.yes : strings.no}, Claude Desktop=${detected.claude_desktop.installed ? strings.yes : strings.no}`);
|
|
843
|
+
setup.interactive_clients.claude_code = await askYesNo(strings.useClaudeCodeQ, detected.claude_code.installed || true);
|
|
844
|
+
setup.interactive_clients.codex = await askYesNo(strings.useCodexQ, detected.codex.installed);
|
|
845
|
+
setup.interactive_clients.claude_desktop = await askYesNo(strings.useDesktopQ, detected.claude_desktop.installed);
|
|
846
|
+
|
|
847
|
+
const defaultTerminalChoices = [
|
|
848
|
+
{ value: "claude_code", label: lang === "es" ? "Claude Code (recomendado)" : "Claude Code (recommended)" },
|
|
849
|
+
{ value: "codex", label: "Codex" },
|
|
850
|
+
].filter((item) => setup.interactive_clients[item.value]);
|
|
851
|
+
|
|
852
|
+
if (defaultTerminalChoices.length === 1) {
|
|
853
|
+
setup.default_terminal_client = defaultTerminalChoices[0].value;
|
|
854
|
+
} else if (defaultTerminalChoices.length > 1) {
|
|
855
|
+
setup.default_terminal_client = await askChoice(
|
|
856
|
+
strings.defaultTerminalQ,
|
|
857
|
+
defaultTerminalChoices,
|
|
858
|
+
setup.interactive_clients.codex && !setup.interactive_clients.claude_code ? "codex" : "claude_code",
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
setup.automation_enabled = await askYesNo(strings.automationQ, true);
|
|
863
|
+
if (setup.automation_enabled) {
|
|
864
|
+
const backendDefault = setup.interactive_clients.codex && !setup.interactive_clients.claude_code ? "codex" : "claude_code";
|
|
865
|
+
setup.automation_backend = await askChoice(
|
|
866
|
+
strings.automationBackendQ,
|
|
867
|
+
[
|
|
868
|
+
{ value: "claude_code", label: lang === "es" ? "Claude Code (recomendado)" : "Claude Code (recommended)" },
|
|
869
|
+
{ value: "codex", label: "Codex" },
|
|
870
|
+
],
|
|
871
|
+
backendDefault,
|
|
872
|
+
);
|
|
873
|
+
} else {
|
|
874
|
+
setup.automation_backend = "none";
|
|
875
|
+
}
|
|
876
|
+
console.log("");
|
|
877
|
+
} else if (detected.codex.installed) {
|
|
878
|
+
setup.interactive_clients.codex = true;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const required = requiredCliClients(setup);
|
|
882
|
+
for (const client of required) {
|
|
883
|
+
if (detected[client] && detected[client].installed) continue;
|
|
884
|
+
let shouldInstall = useDefaults || autoInstall === "auto";
|
|
885
|
+
if (!shouldInstall && process.stdin.isTTY && process.stdout.isTTY) {
|
|
886
|
+
const question = client === "claude_code" ? strings.installClaudeQ : strings.installCodexQ;
|
|
887
|
+
shouldInstall = await askYesNo(question, true);
|
|
888
|
+
}
|
|
889
|
+
if (!shouldInstall) continue;
|
|
890
|
+
log(client === "claude_code" ? strings.installingClaude : strings.installingCodex);
|
|
891
|
+
const outcome = client === "claude_code" ? installClaudeCodeCli(process.platform) : installCodexCli();
|
|
892
|
+
detected = detectInstalledClients();
|
|
893
|
+
if (outcome.installed && client === "claude_code") {
|
|
894
|
+
log("Claude Code installed successfully.");
|
|
895
|
+
} else if (outcome.installed && client === "codex") {
|
|
896
|
+
log("Codex installed successfully.");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (setup.default_terminal_client && !detected[setup.default_terminal_client]?.installed) {
|
|
901
|
+
const fallback = ["claude_code", "codex"].find((key) => key !== setup.default_terminal_client && detected[key]?.installed && setup.interactive_clients[key]);
|
|
902
|
+
if (fallback) {
|
|
903
|
+
setup.default_terminal_client = fallback;
|
|
904
|
+
log(`Default terminal client fallback: ${fallback}`);
|
|
905
|
+
} else {
|
|
906
|
+
const label = setup.default_terminal_client === "claude_code" ? "Claude Code" : "Codex";
|
|
907
|
+
log(strings.terminalFallback(label));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (setup.automation_enabled && setup.automation_backend !== "none" && !detected[setup.automation_backend]?.installed) {
|
|
912
|
+
const label = setup.automation_backend === "claude_code" ? "Claude Code" : "Codex";
|
|
913
|
+
log(strings.automationDisabled(label));
|
|
914
|
+
setup.automation_enabled = false;
|
|
915
|
+
setup.automation_backend = "none";
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (!detected.claude_desktop.installed && setup.interactive_clients.claude_desktop) {
|
|
919
|
+
log(strings.desktopManual);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!useDefaults) {
|
|
923
|
+
const activeRuntimeClients = Array.from(new Set([
|
|
924
|
+
setup.default_terminal_client,
|
|
925
|
+
...(setup.automation_enabled && setup.automation_backend !== "none" ? [setup.automation_backend] : []),
|
|
926
|
+
].filter(Boolean)));
|
|
927
|
+
for (const client of activeRuntimeClients) {
|
|
928
|
+
setup.client_runtime_profiles[client] = await askClientRuntimeProfile({
|
|
929
|
+
lang,
|
|
930
|
+
client,
|
|
931
|
+
currentProfile: setup.client_runtime_profiles[client] || defaultClientRuntimeProfiles()[client] || {},
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const defaultProfile = formatRuntimeProfile(
|
|
937
|
+
setup.client_runtime_profiles[setup.default_terminal_client] || defaultClientRuntimeProfiles()[setup.default_terminal_client] || {}
|
|
938
|
+
);
|
|
939
|
+
const backendProfile = setup.automation_enabled && setup.automation_backend !== "none"
|
|
940
|
+
? formatRuntimeProfile(
|
|
941
|
+
setup.client_runtime_profiles[setup.automation_backend] || defaultClientRuntimeProfiles()[setup.automation_backend] || {}
|
|
942
|
+
)
|
|
943
|
+
: "";
|
|
944
|
+
log(strings.summary(setup.default_terminal_client, defaultProfile, setup.automation_backend, backendProfile, setup.automation_enabled));
|
|
945
|
+
return { setup, detected };
|
|
946
|
+
}
|
|
947
|
+
|
|
520
948
|
async function maybeConfigurePowerPolicy(schedule, useDefaults) {
|
|
521
949
|
const current = String((schedule && schedule.power_policy) || "unset").toLowerCase();
|
|
522
950
|
if (current && current !== "unset") {
|
|
@@ -1293,39 +1721,12 @@ async function main() {
|
|
|
1293
1721
|
log(`Found ${pyVersion} at ${python}`);
|
|
1294
1722
|
logMacPermissionsNotice(NEXO_HOME, python);
|
|
1295
1723
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
claudeInstalled = run("which claude");
|
|
1303
|
-
if (!claudeInstalled) {
|
|
1304
|
-
// Fallback: npm -g (may need sudo on Linux)
|
|
1305
|
-
const npmCmd = platform === "linux" ? "sudo" : "npm";
|
|
1306
|
-
const npmArgs = platform === "linux" ? ["npm", "install", "-g", "@anthropic-ai/claude-code"] : ["install", "-g", "@anthropic-ai/claude-code"];
|
|
1307
|
-
spawnSync(npmCmd, npmArgs, { stdio: "inherit" });
|
|
1308
|
-
claudeInstalled = run("which claude");
|
|
1309
|
-
}
|
|
1310
|
-
if (!claudeInstalled) {
|
|
1311
|
-
log("Could not install Claude Code automatically.");
|
|
1312
|
-
log("Install it manually: npm install -g @anthropic-ai/claude-code");
|
|
1313
|
-
log("(On Linux you may need: sudo npm install -g @anthropic-ai/claude-code)");
|
|
1314
|
-
process.exit(1);
|
|
1315
|
-
}
|
|
1316
|
-
log("Claude Code installed successfully.");
|
|
1317
|
-
} else {
|
|
1318
|
-
log("Claude Code detected.");
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// Persist the discovered claude CLI path for scheduled scripts
|
|
1322
|
-
const claudeCliPath = run("which claude") || "";
|
|
1323
|
-
if (claudeCliPath) {
|
|
1324
|
-
const cliPathFile = path.join(NEXO_HOME, "config", "claude-cli-path");
|
|
1325
|
-
fs.mkdirSync(path.join(NEXO_HOME, "config"), { recursive: true });
|
|
1326
|
-
fs.writeFileSync(cliPathFile, claudeCliPath.trim());
|
|
1327
|
-
log(`Claude CLI path saved: ${claudeCliPath.trim()}`);
|
|
1328
|
-
}
|
|
1724
|
+
let detectedClients = detectInstalledClients();
|
|
1725
|
+
log(
|
|
1726
|
+
`Client detection: Claude Code=${detectedClients.claude_code.installed ? "yes" : "no"}, `
|
|
1727
|
+
+ `Codex=${detectedClients.codex.installed ? "yes" : "no"}, `
|
|
1728
|
+
+ `Claude Desktop=${detectedClients.claude_desktop.installed ? "yes" : "no"}`
|
|
1729
|
+
);
|
|
1329
1730
|
console.log("");
|
|
1330
1731
|
|
|
1331
1732
|
// Step 1: Language (P1)
|
|
@@ -1634,7 +2035,7 @@ async function main() {
|
|
|
1634
2035
|
let doScan = false;
|
|
1635
2036
|
let doCaffeinate = false;
|
|
1636
2037
|
let doDashboard = false;
|
|
1637
|
-
let autoInstall = "ask";
|
|
2038
|
+
let autoInstall = useDefaults ? "auto" : "ask";
|
|
1638
2039
|
if (!useDefaults) {
|
|
1639
2040
|
const scanAnswer = await ask(t.scanQ);
|
|
1640
2041
|
doScan = scanAnswer.trim() === "1" || scanAnswer.trim().toLowerCase().startsWith("y") || scanAnswer.trim().toLowerCase().startsWith("s");
|
|
@@ -1665,6 +2066,15 @@ async function main() {
|
|
|
1665
2066
|
console.log("");
|
|
1666
2067
|
}
|
|
1667
2068
|
|
|
2069
|
+
const clientConfig = await configureClientSetup({
|
|
2070
|
+
lang,
|
|
2071
|
+
useDefaults,
|
|
2072
|
+
autoInstall,
|
|
2073
|
+
detected: detectedClients,
|
|
2074
|
+
});
|
|
2075
|
+
const clientSetup = clientConfig.setup;
|
|
2076
|
+
detectedClients = clientConfig.detected;
|
|
2077
|
+
|
|
1668
2078
|
// Step 3: Install Python dependencies (use venv to avoid PEP 668 on modern Linux)
|
|
1669
2079
|
log("Installing cognitive engine dependencies...");
|
|
1670
2080
|
fs.mkdirSync(NEXO_HOME, { recursive: true });
|
|
@@ -2372,16 +2782,28 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
2372
2782
|
|
|
2373
2783
|
const syncClientsScript = path.join(NEXO_HOME, "scripts", "nexo-sync-clients.py");
|
|
2374
2784
|
if (fs.existsSync(syncClientsScript)) {
|
|
2785
|
+
const syncArgs = [
|
|
2786
|
+
syncClientsScript,
|
|
2787
|
+
"--nexo-home", NEXO_HOME,
|
|
2788
|
+
"--runtime-root", NEXO_HOME,
|
|
2789
|
+
"--python", python,
|
|
2790
|
+
"--operator-name", operatorName,
|
|
2791
|
+
];
|
|
2792
|
+
const enabledSyncClients = Array.from(new Set([
|
|
2793
|
+
...Object.entries(clientSetup.interactive_clients)
|
|
2794
|
+
.filter(([, enabled]) => Boolean(enabled))
|
|
2795
|
+
.map(([key]) => key),
|
|
2796
|
+
...(clientSetup.automation_enabled && clientSetup.automation_backend !== "none"
|
|
2797
|
+
? [clientSetup.automation_backend]
|
|
2798
|
+
: []),
|
|
2799
|
+
]));
|
|
2800
|
+
enabledSyncClients.forEach((client) => {
|
|
2801
|
+
syncArgs.push("--enabled-client", client);
|
|
2802
|
+
});
|
|
2803
|
+
syncArgs.push("--json");
|
|
2375
2804
|
const syncResult = spawnSync(
|
|
2376
2805
|
python,
|
|
2377
|
-
|
|
2378
|
-
syncClientsScript,
|
|
2379
|
-
"--nexo-home", NEXO_HOME,
|
|
2380
|
-
"--runtime-root", NEXO_HOME,
|
|
2381
|
-
"--python", python,
|
|
2382
|
-
"--operator-name", operatorName,
|
|
2383
|
-
"--json",
|
|
2384
|
-
],
|
|
2806
|
+
syncArgs,
|
|
2385
2807
|
{ encoding: "utf8" }
|
|
2386
2808
|
);
|
|
2387
2809
|
if (syncResult.status === 0) {
|
|
@@ -2404,13 +2826,23 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
2404
2826
|
}
|
|
2405
2827
|
}
|
|
2406
2828
|
|
|
2829
|
+
const claudeCliPath = run("which claude") || "";
|
|
2830
|
+
if (claudeCliPath) {
|
|
2831
|
+
const cliPathFile = path.join(NEXO_HOME, "config", "claude-cli-path");
|
|
2832
|
+
fs.mkdirSync(path.dirname(cliPathFile), { recursive: true });
|
|
2833
|
+
fs.writeFileSync(cliPathFile, claudeCliPath.trim());
|
|
2834
|
+
log(`Claude CLI path saved: ${claudeCliPath.trim()}`);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2407
2837
|
// Step 7: Create schedule.json (only on fresh install) and install core processes
|
|
2408
2838
|
log("Setting up automated processes...");
|
|
2409
2839
|
let schedule = loadOrCreateSchedule(NEXO_HOME);
|
|
2840
|
+
schedule = applyClientSetupToSchedule(schedule, clientSetup);
|
|
2841
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
2410
2842
|
schedule = await maybeConfigurePowerPolicy(schedule, useDefaults);
|
|
2411
2843
|
schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
|
|
2412
2844
|
schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
|
|
2413
|
-
const enabledOptionals = { dashboard: doDashboard };
|
|
2845
|
+
const enabledOptionals = { dashboard: doDashboard, automation: schedule.automation_enabled !== false };
|
|
2414
2846
|
if (isEphemeralInstall(NEXO_HOME)) {
|
|
2415
2847
|
log("Ephemeral HOME/NEXO_HOME detected — skipping LaunchAgents installation.");
|
|
2416
2848
|
} else {
|
|
@@ -2431,13 +2863,8 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
2431
2863
|
// Step 8: Create shell alias and add runtime CLI to PATH
|
|
2432
2864
|
log("Creating shell alias...");
|
|
2433
2865
|
const aliasName = operatorName.toLowerCase();
|
|
2434
|
-
const
|
|
2435
|
-
|
|
2436
|
-
try { return fs.readFileSync(p, "utf8").trim(); } catch { return ""; }
|
|
2437
|
-
})();
|
|
2438
|
-
const claudeBin = savedCliPath || run("which claude") || "claude";
|
|
2439
|
-
const aliasLine = `alias ${aliasName}='${claudeBin} --dangerously-skip-permissions "."'`;
|
|
2440
|
-
const aliasComment = `# ${operatorName} — start Claude Code with ${operatorName} speaking first`;
|
|
2866
|
+
const aliasLine = `alias ${aliasName}='nexo chat .'`;
|
|
2867
|
+
const aliasComment = `# ${operatorName} — open the configured NEXO terminal client`;
|
|
2441
2868
|
const nexoPathLine = `export PATH="${path.join(NEXO_HOME, "bin")}:$PATH"`;
|
|
2442
2869
|
const nexoPathComment = "# NEXO runtime CLI";
|
|
2443
2870
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.13",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — local cognitive runtime for Claude Code. Persistent memory, overnight learning, recovery-aware crons, personal scripts, doctor diagnostics, startup preflight, and optional power helper.",
|
|
6
6
|
"bin": {
|
|
@@ -72,6 +72,9 @@
|
|
|
72
72
|
"!src/**/*.pyc",
|
|
73
73
|
"!src/**/*.pyo",
|
|
74
74
|
"templates/",
|
|
75
|
+
"!templates/**/__pycache__",
|
|
76
|
+
"!templates/**/*.pyc",
|
|
77
|
+
"!templates/**/*.pyo",
|
|
75
78
|
".claude-plugin/",
|
|
76
79
|
".mcp.json",
|
|
77
80
|
"hooks/hooks.json",
|