nexo-brain 2.6.10 → 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
@@ -89,6 +89,46 @@ function syncWatchdogHashRegistry(nexoHome) {
89
89
  }
90
90
  }
91
91
 
92
+ function getCoreRuntimeFlatFiles() {
93
+ return [
94
+ "server.py",
95
+ "plugin_loader.py",
96
+ "knowledge_graph.py",
97
+ "kg_populate.py",
98
+ "maintenance.py",
99
+ "storage_router.py",
100
+ "claim_graph.py",
101
+ "hnsw_index.py",
102
+ "evolution_cycle.py",
103
+ "migrate_embeddings.py",
104
+ "auto_close_sessions.py",
105
+ "client_sync.py",
106
+ "client_preferences.py",
107
+ "agent_runner.py",
108
+ "auto_update.py",
109
+ "tools_sessions.py",
110
+ "tools_coordination.py",
111
+ "tools_reminders.py",
112
+ "tools_reminders_crud.py",
113
+ "tools_learnings.py",
114
+ "tools_credentials.py",
115
+ "tools_task_history.py",
116
+ "tools_menu.py",
117
+ "cli.py",
118
+ "script_registry.py",
119
+ "skills_runtime.py",
120
+ "user_context.py",
121
+ "public_contribution.py",
122
+ "cron_recovery.py",
123
+ "runtime_power.py",
124
+ "requirements.txt",
125
+ ];
126
+ }
127
+
128
+ function getCoreRuntimePackages() {
129
+ return ["db", "cognitive", "doctor"];
130
+ }
131
+
92
132
  function isProtectedMacPath(candidate) {
93
133
  if (process.platform !== "darwin" || !candidate) return false;
94
134
  const homeDir = require("os").homedir();
@@ -270,7 +310,7 @@ const ALL_PROCESSES = [
270
310
  type: "interval", intervalMinutes: 30, purpose: "System immunity checks" },
271
311
  // --- Every 2 hours ---
272
312
  { name: "synthesis", script: "nexo-synthesis.py", interpreter: "python", scriptDir: "scripts",
273
- type: "interval", intervalMinutes: 120, purpose: "Memory synthesis" },
313
+ type: "interval", intervalMinutes: 120, optional: "automation", purpose: "Memory synthesis" },
274
314
  // --- Every hour ---
275
315
  { name: "backup", script: "nexo-backup.sh", interpreter: "bash", scriptDir: "scripts",
276
316
  type: "interval", intervalMinutes: 60, purpose: "DB backups" },
@@ -289,16 +329,16 @@ const ALL_PROCESSES = [
289
329
  { name: "cognitive-decay", script: "nexo-cognitive-decay.py", interpreter: "python", scriptDir: "scripts",
290
330
  type: "daily", defaultHour: 3, defaultMinute: 0, purpose: "Memory decay" },
291
331
  { name: "postmortem", script: "nexo-postmortem-consolidator.py", interpreter: "python", scriptDir: "scripts",
292
- type: "daily", defaultHour: 23, defaultMinute: 30, purpose: "Session consolidation" },
332
+ type: "daily", defaultHour: 23, defaultMinute: 30, optional: "automation", purpose: "Session consolidation" },
293
333
  { name: "self-audit", script: "nexo-daily-self-audit.py", interpreter: "python", scriptDir: "scripts",
294
- type: "daily", defaultHour: 7, defaultMinute: 0, purpose: "Self-diagnostic" },
334
+ type: "daily", defaultHour: 7, defaultMinute: 0, optional: "automation", purpose: "Self-diagnostic" },
295
335
  { name: "sleep", script: "nexo-sleep.py", interpreter: "python", scriptDir: "scripts",
296
- type: "daily", defaultHour: 4, defaultMinute: 0, purpose: "Sleep cycle" },
336
+ type: "daily", defaultHour: 4, defaultMinute: 0, optional: "automation", purpose: "Sleep cycle" },
297
337
  { name: "deep-sleep", script: "nexo-deep-sleep.sh", interpreter: "bash", scriptDir: "scripts",
298
- type: "daily", defaultHour: 4, defaultMinute: 30, purpose: "Deep sleep analysis" },
338
+ type: "daily", defaultHour: 4, defaultMinute: 30, optional: "automation", purpose: "Deep sleep analysis" },
299
339
  // --- Weekly (day + time from schedule.json) ---
300
340
  { name: "evolution", script: "nexo-evolution-run.py", interpreter: "python", scriptDir: "scripts",
301
- 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" },
302
342
  { name: "followup-hygiene", script: "nexo-followup-hygiene.py", interpreter: "python", scriptDir: "scripts",
303
343
  type: "weekly", defaultDay: "sunday", defaultHour: 5, defaultMinute: 0, purpose: "Cleanup stale followups" },
304
344
  ];
@@ -429,6 +469,29 @@ function getDefaultSchedule(timezone) {
429
469
  return {
430
470
  timezone: timezone || "UTC",
431
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
+ },
432
495
  power_policy: "unset",
433
496
  power_policy_version: 2,
434
497
  full_disk_access_status: "unset",
@@ -479,6 +542,409 @@ function normalizePublicContributionConfig(config = {}) {
479
542
  return merged;
480
543
  }
481
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
+
482
948
  async function maybeConfigurePowerPolicy(schedule, useDefaults) {
483
949
  const current = String((schedule && schedule.power_policy) || "unset").toLowerCase();
484
950
  if (current && current !== "unset") {
@@ -981,16 +1447,7 @@ async function main() {
981
1447
  log(" Hooks updated.");
982
1448
 
983
1449
  // Update core Python files (flat .py files in src/)
984
- const coreFlatFiles = [
985
- "server.py", "plugin_loader.py",
986
- "knowledge_graph.py", "kg_populate.py", "maintenance.py", "storage_router.py",
987
- "claim_graph.py", "hnsw_index.py", "evolution_cycle.py", "migrate_embeddings.py",
988
- "auto_close_sessions.py", "auto_update.py",
989
- "tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
990
- "tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
991
- "tools_task_history.py", "tools_menu.py",
992
- "requirements.txt",
993
- ];
1450
+ const coreFlatFiles = getCoreRuntimeFlatFiles();
994
1451
  coreFlatFiles.forEach((f) => {
995
1452
  const src = path.join(srcDir, f);
996
1453
  if (fs.existsSync(src)) {
@@ -998,7 +1455,7 @@ async function main() {
998
1455
  }
999
1456
  });
1000
1457
  // Update core packages (db/, cognitive/) — full directory copy
1001
- ["db", "cognitive"].forEach(pkg => {
1458
+ getCoreRuntimePackages().forEach(pkg => {
1002
1459
  const pkgSrc = path.join(srcDir, pkg);
1003
1460
  if (fs.existsSync(pkgSrc)) {
1004
1461
  copyDirRec(pkgSrc, path.join(NEXO_HOME, pkg));
@@ -1264,39 +1721,12 @@ async function main() {
1264
1721
  log(`Found ${pyVersion} at ${python}`);
1265
1722
  logMacPermissionsNotice(NEXO_HOME, python);
1266
1723
 
1267
- // Find or install Claude Code
1268
- let claudeInstalled = run("which claude");
1269
- if (!claudeInstalled) {
1270
- log("Claude Code not found. Installing...");
1271
- // Try npx first (no sudo needed), then npm -g as fallback
1272
- spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], { stdio: "pipe", timeout: 60000 });
1273
- claudeInstalled = run("which claude");
1274
- if (!claudeInstalled) {
1275
- // Fallback: npm -g (may need sudo on Linux)
1276
- const npmCmd = platform === "linux" ? "sudo" : "npm";
1277
- const npmArgs = platform === "linux" ? ["npm", "install", "-g", "@anthropic-ai/claude-code"] : ["install", "-g", "@anthropic-ai/claude-code"];
1278
- spawnSync(npmCmd, npmArgs, { stdio: "inherit" });
1279
- claudeInstalled = run("which claude");
1280
- }
1281
- if (!claudeInstalled) {
1282
- log("Could not install Claude Code automatically.");
1283
- log("Install it manually: npm install -g @anthropic-ai/claude-code");
1284
- log("(On Linux you may need: sudo npm install -g @anthropic-ai/claude-code)");
1285
- process.exit(1);
1286
- }
1287
- log("Claude Code installed successfully.");
1288
- } else {
1289
- log("Claude Code detected.");
1290
- }
1291
-
1292
- // Persist the discovered claude CLI path for scheduled scripts
1293
- const claudeCliPath = run("which claude") || "";
1294
- if (claudeCliPath) {
1295
- const cliPathFile = path.join(NEXO_HOME, "config", "claude-cli-path");
1296
- fs.mkdirSync(path.join(NEXO_HOME, "config"), { recursive: true });
1297
- fs.writeFileSync(cliPathFile, claudeCliPath.trim());
1298
- log(`Claude CLI path saved: ${claudeCliPath.trim()}`);
1299
- }
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
+ );
1300
1730
  console.log("");
1301
1731
 
1302
1732
  // Step 1: Language (P1)
@@ -1605,7 +2035,7 @@ async function main() {
1605
2035
  let doScan = false;
1606
2036
  let doCaffeinate = false;
1607
2037
  let doDashboard = false;
1608
- let autoInstall = "ask";
2038
+ let autoInstall = useDefaults ? "auto" : "ask";
1609
2039
  if (!useDefaults) {
1610
2040
  const scanAnswer = await ask(t.scanQ);
1611
2041
  doScan = scanAnswer.trim() === "1" || scanAnswer.trim().toLowerCase().startsWith("y") || scanAnswer.trim().toLowerCase().startsWith("s");
@@ -1636,6 +2066,15 @@ async function main() {
1636
2066
  console.log("");
1637
2067
  }
1638
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
+
1639
2078
  // Step 3: Install Python dependencies (use venv to avoid PEP 668 on modern Linux)
1640
2079
  log("Installing cognitive engine dependencies...");
1641
2080
  fs.mkdirSync(NEXO_HOME, { recursive: true });
@@ -1752,33 +2191,7 @@ async function main() {
1752
2191
  };
1753
2192
 
1754
2193
  // Core flat files (single .py files in src/)
1755
- const coreFiles = [
1756
- "server.py",
1757
- "plugin_loader.py",
1758
- "knowledge_graph.py",
1759
- "kg_populate.py",
1760
- "maintenance.py",
1761
- "storage_router.py",
1762
- "claim_graph.py",
1763
- "hnsw_index.py",
1764
- "evolution_cycle.py",
1765
- "migrate_embeddings.py",
1766
- "auto_close_sessions.py",
1767
- "client_sync.py",
1768
- "auto_update.py",
1769
- "tools_sessions.py",
1770
- "tools_coordination.py",
1771
- "tools_reminders.py",
1772
- "tools_reminders_crud.py",
1773
- "tools_learnings.py",
1774
- "tools_credentials.py",
1775
- "tools_task_history.py",
1776
- "tools_menu.py",
1777
- "requirements.txt",
1778
- "cli.py",
1779
- "script_registry.py",
1780
- "skills_runtime.py",
1781
- ];
2194
+ const coreFiles = getCoreRuntimeFlatFiles();
1782
2195
  coreFiles.forEach((f) => {
1783
2196
  const src = path.join(srcDir, f);
1784
2197
  if (fs.existsSync(src)) {
@@ -1811,7 +2224,7 @@ async function main() {
1811
2224
 
1812
2225
  log("Copying core packages...");
1813
2226
  // Core packages (directories with __init__.py)
1814
- ["db", "cognitive", "doctor"].forEach(pkg => {
2227
+ getCoreRuntimePackages().forEach(pkg => {
1815
2228
  const pkgSrc = path.join(srcDir, pkg);
1816
2229
  if (fs.existsSync(pkgSrc)) {
1817
2230
  copyDirRecursive(pkgSrc, path.join(NEXO_HOME, pkg));
@@ -2369,16 +2782,28 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
2369
2782
 
2370
2783
  const syncClientsScript = path.join(NEXO_HOME, "scripts", "nexo-sync-clients.py");
2371
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");
2372
2804
  const syncResult = spawnSync(
2373
2805
  python,
2374
- [
2375
- syncClientsScript,
2376
- "--nexo-home", NEXO_HOME,
2377
- "--runtime-root", NEXO_HOME,
2378
- "--python", python,
2379
- "--operator-name", operatorName,
2380
- "--json",
2381
- ],
2806
+ syncArgs,
2382
2807
  { encoding: "utf8" }
2383
2808
  );
2384
2809
  if (syncResult.status === 0) {
@@ -2401,13 +2826,23 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
2401
2826
  }
2402
2827
  }
2403
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
+
2404
2837
  // Step 7: Create schedule.json (only on fresh install) and install core processes
2405
2838
  log("Setting up automated processes...");
2406
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));
2407
2842
  schedule = await maybeConfigurePowerPolicy(schedule, useDefaults);
2408
2843
  schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
2409
2844
  schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
2410
- const enabledOptionals = { dashboard: doDashboard };
2845
+ const enabledOptionals = { dashboard: doDashboard, automation: schedule.automation_enabled !== false };
2411
2846
  if (isEphemeralInstall(NEXO_HOME)) {
2412
2847
  log("Ephemeral HOME/NEXO_HOME detected — skipping LaunchAgents installation.");
2413
2848
  } else {
@@ -2428,13 +2863,8 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
2428
2863
  // Step 8: Create shell alias and add runtime CLI to PATH
2429
2864
  log("Creating shell alias...");
2430
2865
  const aliasName = operatorName.toLowerCase();
2431
- const savedCliPath = (() => {
2432
- const p = path.join(NEXO_HOME, "config", "claude-cli-path");
2433
- try { return fs.readFileSync(p, "utf8").trim(); } catch { return ""; }
2434
- })();
2435
- const claudeBin = savedCliPath || run("which claude") || "claude";
2436
- const aliasLine = `alias ${aliasName}='${claudeBin} --dangerously-skip-permissions "."'`;
2437
- 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`;
2438
2868
  const nexoPathLine = `export PATH="${path.join(NEXO_HOME, "bin")}:$PATH"`;
2439
2869
  const nexoPathComment = "# NEXO runtime CLI";
2440
2870