nexo-brain 2.6.11 → 2.6.12

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/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
- // Find or install Claude Code
1297
- let claudeInstalled = run("which claude");
1298
- if (!claudeInstalled) {
1299
- log("Claude Code not found. Installing...");
1300
- // Try npx first (no sudo needed), then npm -g as fallback
1301
- spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], { stdio: "pipe", timeout: 60000 });
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 savedCliPath = (() => {
2435
- const p = path.join(NEXO_HOME, "config", "claude-cli-path");
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.11",
3
+ "version": "2.6.12",
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",