nexo-brain 5.10.2 → 6.0.0
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 +3 -1
- package/bin/nexo-brain.js +281 -183
- package/hooks/hooks.json +18 -48
- package/package.json +3 -1
- package/src/auto_update.py +20 -0
- package/src/calibration_migration.py +134 -0
- package/src/hook_observability.py +24 -0
- package/src/hooks/auto_capture.py +312 -91
- package/src/hooks/manifest.json +12 -0
- package/src/hooks/notification.py +78 -0
- package/src/hooks/post_tool_use.py +126 -0
- package/src/hooks/pre_compact.py +50 -0
- package/src/hooks/session_start.py +96 -0
- package/src/hooks/stop.py +50 -0
- package/src/hooks/subagent_stop.py +144 -0
- package/src/protocol_settings.py +45 -41
- package/src/resonance_map.py +69 -25
- package/src/resonance_tiers.json +21 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `
|
|
21
|
+
Version `6.0.0` is the current packaged-runtime line: **BREAKING** tier-only setup. Onboarding asks for one resonance tier (`maximo`/`alto`/`medio`/`bajo`) and that choice drives every backend via `src/resonance_tiers.json`; the per-backend model/effort prompts are gone and the legacy `client_runtime_profiles.{claude_code,codex}.{model,reasoning_effort}` are silently purged from `schedule.json` on upgrade. Protocol strictness is no longer configurable — interactive TTY sessions run `strict`, non-TTY (crons, pipes, tests) run `lenient`; `NEXO_PROTOCOL_STRICTNESS` env, `preferences.protocol_strictness`, and the `default/normal/off/warn/soft` aliases are all removed. `preferences.show_pending_at_start` moves to NEXO Desktop's electron-store. The seven core hooks are now unified behind `src/hooks/manifest.json` (plugin and npm modes read the same file), two new hooks ship (`Notification` for live-session activity and `SubagentStop` for auto-closing stale `protocol_tasks`), and `auto_capture.py` is wired to both `UserPromptSubmit` and `PostToolUse` with a persistent 1h dedup table plus an automatic `nexo_learning_add` on correction matches. `~/.nexo/hooks_status.json` is published after every `registerAllCoreHooks()` so NEXO Desktop ≥0.12.0 can render Hooks activos X/Y. New `nexo-brain --skip` flag aliases `--yes`/`--defaults`. Full suite 1057 passed, 1 skipped.
|
|
22
|
+
|
|
23
|
+
Previously in `5.10.2`: auto-bootstraps `brain/profile.json` from `brain/calibration.json` on `nexo update` when the profile file is missing, empty, or corrupt AND calibration carries at least one of `meta.role`, `meta.technical_level`, `name`, `language`. NEXO Desktop's *Preferencias → Avanzado* tab used to render an empty `{}` for that block when the onboarding flow had been interrupted; now it either shows the seeded profile or a friendly explanation of what each file is for, paired with Desktop `v0.11.2` which adds header descriptions to both JSON blocks. Never overwrites a populated profile, never raises, idempotent. Also fixes a latent host-filesystem leak in `test_user_facing_caller_with_no_user_default_uses_alto` exposed by the v5.10.1 migration.
|
|
22
24
|
|
|
23
25
|
Previously in `5.10.1`: silent, one-shot migration that recovers legacy `reasoning_effort="max"` (written by `nexo preferences --reasoning-effort max` before v5.9.0) into the new `preferences.default_resonance` map — any user who had configured `max` before v5.9.0 and never touched the new selector was silently falling back to `DEFAULT_RESONANCE="alto"` on interactive calls since the v5.10.0 update. `_run_runtime_post_sync()` runs `_migrate_effort_to_resonance()` exactly once: `max→maximo`, `xhigh→alto`, `high→medio`, `medium→bajo`. No-op when calibration or schedule already declares an explicit `default_resonance`; idempotent; conservative; never raises.
|
|
24
26
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -60,6 +60,38 @@ const DEFAULT_CLAUDE_CODE_REASONING_EFFORT = _MODEL_DEFAULTS.claude_code.reasoni
|
|
|
60
60
|
const DEFAULT_CODEX_MODEL = _MODEL_DEFAULTS.codex.model;
|
|
61
61
|
const DEFAULT_CODEX_REASONING_EFFORT = _MODEL_DEFAULTS.codex.reasoning_effort || "";
|
|
62
62
|
|
|
63
|
+
// v6.0.0 — Hook manifest is the single source of truth for which hook
|
|
64
|
+
// handlers get registered. Both plugin mode (hooks/hooks.json) and npm
|
|
65
|
+
// mode (this installer's registerAllCoreHooks) read from the same file.
|
|
66
|
+
const HOOKS_MANIFEST_PATH = path.join(__dirname, "..", "src", "hooks", "manifest.json");
|
|
67
|
+
function _loadHooksManifest() {
|
|
68
|
+
try {
|
|
69
|
+
const raw = JSON.parse(fs.readFileSync(HOOKS_MANIFEST_PATH, "utf8"));
|
|
70
|
+
if (raw && Array.isArray(raw.hooks)) {
|
|
71
|
+
return raw;
|
|
72
|
+
}
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
return { version: "1.0", hooks: [] };
|
|
75
|
+
}
|
|
76
|
+
const _HOOKS_MANIFEST = _loadHooksManifest();
|
|
77
|
+
|
|
78
|
+
// v6.0.0 — Resonance tiers JSON holds the (tier → backend → model+effort)
|
|
79
|
+
// mapping. The installer only reads ``default_tier`` and ``tiers`` keys;
|
|
80
|
+
// the real resolution happens on the Python side via resonance_map.py.
|
|
81
|
+
const RESONANCE_TIERS_PATH = path.join(__dirname, "..", "src", "resonance_tiers.json");
|
|
82
|
+
function _loadResonanceTiers() {
|
|
83
|
+
try {
|
|
84
|
+
const raw = JSON.parse(fs.readFileSync(RESONANCE_TIERS_PATH, "utf8"));
|
|
85
|
+
if (raw && raw.tiers && typeof raw.tiers === "object") {
|
|
86
|
+
return raw;
|
|
87
|
+
}
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
return { tiers: {}, default_tier: "alto" };
|
|
90
|
+
}
|
|
91
|
+
const _RESONANCE_TIERS = _loadResonanceTiers();
|
|
92
|
+
const RESONANCE_TIER_NAMES = ["maximo", "alto", "medio", "bajo"];
|
|
93
|
+
const DEFAULT_RESONANCE_TIER = _RESONANCE_TIERS.default_tier || "alto";
|
|
94
|
+
|
|
63
95
|
function isEphemeralInstall(nexoHome) {
|
|
64
96
|
const homeDir = require("os").homedir();
|
|
65
97
|
const allowEphemeral = process.env.NEXO_ALLOW_EPHEMERAL_INSTALL === "1";
|
|
@@ -203,6 +235,7 @@ function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
|
|
|
203
235
|
"runtime_power.py",
|
|
204
236
|
"requirements.txt",
|
|
205
237
|
"model_defaults.json",
|
|
238
|
+
"resonance_tiers.json",
|
|
206
239
|
];
|
|
207
240
|
const discoveredRootModules = fs.existsSync(srcDir)
|
|
208
241
|
? fs.readdirSync(srcDir)
|
|
@@ -494,73 +527,106 @@ const ALL_PROCESSES = [
|
|
|
494
527
|
* key: unique identifier to detect if already registered (avoids duplicates)
|
|
495
528
|
* timeout: seconds before Claude Code kills the hook (prevents hangs)
|
|
496
529
|
*/
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
530
|
+
// v6.0.0 — Core hook list is driven entirely by src/hooks/manifest.json.
|
|
531
|
+
// Each entry declares the Claude Code event and the relative path to the
|
|
532
|
+
// .py handler inside the installed runtime. Every handler receives a
|
|
533
|
+
// short alias key used to detect existing registrations in settings.hooks.
|
|
534
|
+
const HOOK_TIMEOUTS = {
|
|
535
|
+
SessionStart: 40,
|
|
536
|
+
Stop: 15,
|
|
537
|
+
PreCompact: 15,
|
|
538
|
+
PostCompact: 15,
|
|
539
|
+
UserPromptSubmit: 5,
|
|
540
|
+
PostToolUse: 20,
|
|
541
|
+
Notification: 3,
|
|
542
|
+
SubagentStop: 10,
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
function _manifestHookEntries() {
|
|
546
|
+
return (_HOOKS_MANIFEST.hooks || []).map((entry) => {
|
|
547
|
+
const handlerRel = String(entry.handler || "").trim();
|
|
548
|
+
const handlerBase = handlerRel.split("/").pop() || handlerRel;
|
|
549
|
+
return {
|
|
550
|
+
event: entry.event,
|
|
551
|
+
handler: handlerRel,
|
|
552
|
+
key: handlerBase,
|
|
553
|
+
critical: Boolean(entry.critical),
|
|
554
|
+
timeout: HOOK_TIMEOUTS[entry.event] || 10,
|
|
555
|
+
};
|
|
556
|
+
}).filter((h) => h.event && h.handler);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function _hookCommand(hook, hooksDir, nexoHome) {
|
|
560
|
+
// Resolve handler path under the installed runtime. hooksDir points to
|
|
561
|
+
// ~/.nexo/hooks, which is the copy of src/hooks/ at install time.
|
|
562
|
+
const handlerFile = path.basename(hook.handler);
|
|
563
|
+
const runtimePath = path.join(hooksDir, handlerFile);
|
|
564
|
+
return `NEXO_HOME=${nexoHome} python3 ${runtimePath}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function _writeHooksStatus(nexoHome, manifestEntries, registrations) {
|
|
568
|
+
// Publish ~/.nexo/hooks_status.json so NEXO Desktop can render the
|
|
569
|
+
// "Hooks activos X/Y" widget without peeking into settings.json.
|
|
570
|
+
try {
|
|
571
|
+
const now = new Date();
|
|
572
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
573
|
+
const total = manifestEntries.length;
|
|
574
|
+
const registered = registrations.filter((r) => r.status === "active").length;
|
|
575
|
+
const healthy = total > 0 && registered === total;
|
|
576
|
+
const payload = {
|
|
577
|
+
generated_at: now.toISOString().replace(/\.\d+Z$/, "Z"),
|
|
578
|
+
nexo_version: pkgJson.version || "unknown",
|
|
579
|
+
total,
|
|
580
|
+
registered,
|
|
581
|
+
healthy,
|
|
582
|
+
hooks: registrations,
|
|
583
|
+
};
|
|
584
|
+
fs.mkdirSync(nexoHome, { recursive: true });
|
|
585
|
+
fs.writeFileSync(
|
|
586
|
+
path.join(nexoHome, "hooks_status.json"),
|
|
587
|
+
JSON.stringify(payload, null, 2) + "\n",
|
|
588
|
+
);
|
|
589
|
+
} catch (_) {}
|
|
590
|
+
}
|
|
518
591
|
|
|
519
592
|
/**
|
|
520
|
-
* Register
|
|
521
|
-
*
|
|
593
|
+
* Register every hook declared by src/hooks/manifest.json into the
|
|
594
|
+
* Claude Code settings file. Idempotent, never removes user-owned hooks.
|
|
595
|
+
* Writes ~/.nexo/hooks_status.json after each run so NEXO Desktop can
|
|
596
|
+
* display hook health without parsing settings.json.
|
|
522
597
|
*/
|
|
523
598
|
function registerAllCoreHooks(settings, hooksDir, nexoHome) {
|
|
524
599
|
if (!settings.hooks) settings.hooks = {};
|
|
525
600
|
|
|
526
|
-
// Ensure operations dir exists for
|
|
527
|
-
|
|
528
|
-
fs.mkdirSync(
|
|
601
|
+
// Ensure operations dir exists for any hook that wants to drop a file there
|
|
602
|
+
// (session-start.py writes .session-start-ts here).
|
|
603
|
+
fs.mkdirSync(path.join(nexoHome, "operations"), { recursive: true });
|
|
529
604
|
|
|
530
|
-
|
|
531
|
-
|
|
605
|
+
const manifestEntries = _manifestHookEntries();
|
|
606
|
+
const registrations = [];
|
|
532
607
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (hook.commandTemplate) {
|
|
536
|
-
command = hook.commandTemplate(nexoHome);
|
|
537
|
-
} else {
|
|
538
|
-
command = `NEXO_HOME=${nexoHome} bash ${path.join(hooksDir, hook.script)}`;
|
|
539
|
-
}
|
|
608
|
+
for (const hook of manifestEntries) {
|
|
609
|
+
if (!settings.hooks[hook.event]) settings.hooks[hook.event] = [];
|
|
540
610
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
// Nested: [{matcher:"*", hooks:[{type:"command", command:"..."}]}]
|
|
544
|
-
// We need to search and update in both formats.
|
|
611
|
+
const command = _hookCommand(hook, hooksDir, nexoHome);
|
|
612
|
+
let status = "active";
|
|
545
613
|
let found = false;
|
|
546
614
|
|
|
547
615
|
for (let idx = 0; idx < settings.hooks[hook.event].length; idx++) {
|
|
548
616
|
const entry = settings.hooks[hook.event][idx];
|
|
549
617
|
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
550
|
-
// Nested format: {matcher, hooks: [...]}
|
|
551
618
|
if (!entry.matcher) entry.matcher = "*";
|
|
552
619
|
const subIdx = entry.hooks.findIndex(
|
|
553
|
-
(h) => h.command && h.command.includes(hook.key)
|
|
620
|
+
(h) => h.command && h.command.includes(hook.key),
|
|
554
621
|
);
|
|
555
622
|
if (subIdx !== -1) {
|
|
556
623
|
const existing = entry.hooks[subIdx];
|
|
557
624
|
if (existing.command !== command) existing.command = command;
|
|
558
|
-
if (hook.timeout
|
|
625
|
+
if (hook.timeout) existing.timeout = hook.timeout;
|
|
559
626
|
found = true;
|
|
560
627
|
break;
|
|
561
628
|
}
|
|
562
629
|
} else if (entry.command && entry.command.includes(hook.key)) {
|
|
563
|
-
// Legacy flat format: migrate to nested matcher+hooks.
|
|
564
630
|
const migrated = { type: "command", command };
|
|
565
631
|
if (hook.timeout) migrated.timeout = hook.timeout;
|
|
566
632
|
settings.hooks[hook.event][idx] = {
|
|
@@ -580,6 +646,66 @@ function registerAllCoreHooks(settings, hooksDir, nexoHome) {
|
|
|
580
646
|
hooks: [newHook],
|
|
581
647
|
});
|
|
582
648
|
}
|
|
649
|
+
|
|
650
|
+
// Confirm the handler file exists on disk; if not, mark error.
|
|
651
|
+
const handlerAbs = path.join(hooksDir, path.basename(hook.handler));
|
|
652
|
+
if (!fs.existsSync(handlerAbs)) {
|
|
653
|
+
status = "error";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
registrations.push({
|
|
657
|
+
event: hook.event,
|
|
658
|
+
handler: path.basename(hook.handler),
|
|
659
|
+
status,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
_writeHooksStatus(nexoHome, manifestEntries, registrations);
|
|
664
|
+
|
|
665
|
+
// v6.0.0 — also purge any stale v5.x hook commands that referenced the
|
|
666
|
+
// old .sh scripts directly (post-compact.sh, heartbeat-user-msg.sh,
|
|
667
|
+
// protocol-guardrail.sh, etc.) so a pre-existing install migrates
|
|
668
|
+
// cleanly to the manifest-driven world. Only removes NEXO-owned
|
|
669
|
+
// entries, leaves user-custom hooks alone.
|
|
670
|
+
const LEGACY_KEYS = [
|
|
671
|
+
"daily-briefing-check.sh",
|
|
672
|
+
"capture-tool-logs.sh",
|
|
673
|
+
"capture-session.sh",
|
|
674
|
+
"inbox-hook.sh",
|
|
675
|
+
"heartbeat-posttool.sh",
|
|
676
|
+
"heartbeat-user-msg.sh",
|
|
677
|
+
"protocol-guardrail.sh",
|
|
678
|
+
"protocol-pretool-guardrail.sh",
|
|
679
|
+
"post-compact.sh",
|
|
680
|
+
".session-start-ts",
|
|
681
|
+
];
|
|
682
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
683
|
+
const entries = settings.hooks[event];
|
|
684
|
+
if (!Array.isArray(entries)) continue;
|
|
685
|
+
const manifestEventHandlers = new Set(
|
|
686
|
+
manifestEntries.filter((h) => h.event === event).map((h) => h.key),
|
|
687
|
+
);
|
|
688
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
689
|
+
const entry = entries[i];
|
|
690
|
+
if (entry && entry.hooks && Array.isArray(entry.hooks)) {
|
|
691
|
+
entry.hooks = entry.hooks.filter((h) => {
|
|
692
|
+
const cmd = String(h.command || "");
|
|
693
|
+
// Keep anything the manifest owns.
|
|
694
|
+
if (Array.from(manifestEventHandlers).some((k) => cmd.includes(k))) {
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
// Drop strictly-legacy NEXO-owned commands.
|
|
698
|
+
if (LEGACY_KEYS.some((legacy) => cmd.includes(legacy))) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
return true;
|
|
702
|
+
});
|
|
703
|
+
if (entry.hooks.length === 0) {
|
|
704
|
+
entries.splice(i, 1);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (entries.length === 0) delete settings.hooks[event];
|
|
583
709
|
}
|
|
584
710
|
}
|
|
585
711
|
|
|
@@ -620,15 +746,14 @@ function getDefaultSchedule(timezone) {
|
|
|
620
746
|
default_terminal_client: "claude_code",
|
|
621
747
|
automation_enabled: true,
|
|
622
748
|
automation_backend: "claude_code",
|
|
749
|
+
// v6.0.0 — model/reasoning_effort have moved to src/resonance_tiers.json
|
|
750
|
+
// keyed by the operator's preferences.default_resonance. The shape
|
|
751
|
+
// below stays so that downstream readers that iterate the profile
|
|
752
|
+
// dict do not need a guard, but the concrete values no longer live
|
|
753
|
+
// in schedule.json.
|
|
623
754
|
client_runtime_profiles: {
|
|
624
|
-
claude_code: {
|
|
625
|
-
|
|
626
|
-
reasoning_effort: DEFAULT_CLAUDE_CODE_REASONING_EFFORT,
|
|
627
|
-
},
|
|
628
|
-
codex: {
|
|
629
|
-
model: DEFAULT_CODEX_MODEL,
|
|
630
|
-
reasoning_effort: DEFAULT_CODEX_REASONING_EFFORT,
|
|
631
|
-
},
|
|
755
|
+
claude_code: {},
|
|
756
|
+
codex: {},
|
|
632
757
|
},
|
|
633
758
|
client_install_preferences: {
|
|
634
759
|
claude_code: "ask",
|
|
@@ -796,15 +921,12 @@ async function askChoice(question, options, defaultValue) {
|
|
|
796
921
|
}
|
|
797
922
|
|
|
798
923
|
function defaultClientRuntimeProfiles() {
|
|
924
|
+
// v6.0.0 — no more model/reasoning_effort here. The resonance tier
|
|
925
|
+
// (preferences.default_resonance in calibration.json) plus
|
|
926
|
+
// src/resonance_tiers.json drive the actual model and effort at runtime.
|
|
799
927
|
return {
|
|
800
|
-
claude_code: {
|
|
801
|
-
|
|
802
|
-
reasoning_effort: DEFAULT_CLAUDE_CODE_REASONING_EFFORT,
|
|
803
|
-
},
|
|
804
|
-
codex: {
|
|
805
|
-
model: DEFAULT_CODEX_MODEL,
|
|
806
|
-
reasoning_effort: DEFAULT_CODEX_REASONING_EFFORT,
|
|
807
|
-
},
|
|
928
|
+
claude_code: {},
|
|
929
|
+
codex: {},
|
|
808
930
|
};
|
|
809
931
|
}
|
|
810
932
|
|
|
@@ -820,85 +942,23 @@ function formatRuntimeProfile(profile = {}) {
|
|
|
820
942
|
return effort ? `${model}/${effort}` : model;
|
|
821
943
|
}
|
|
822
944
|
|
|
823
|
-
|
|
945
|
+
// v6.0.0 — Tier-only setup. Onboarding asks the operator for one resonance
|
|
946
|
+
// tier (maximo / alto / medio / bajo) and that choice drives every backend
|
|
947
|
+
// via src/resonance_tiers.json. No more model or effort questions.
|
|
948
|
+
async function askResonanceTier(lang, currentTier) {
|
|
824
949
|
const recommended = lang === "es" ? " (recomendado)" : " (recommended)";
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
modelOptions: [
|
|
838
|
-
{ value: DEFAULT_CLAUDE_CODE_MODEL, label: `Opus 4.6 with 1M context${recommended}` },
|
|
839
|
-
{ value: "claude-opus-4-6", label: "Opus 4.6" },
|
|
840
|
-
{ value: "sonnet", label: "Sonnet latest" },
|
|
841
|
-
{ value: "custom", label: lang === "es" ? "Modelo personalizado" : "Custom model" },
|
|
842
|
-
],
|
|
843
|
-
effortOptions: [
|
|
844
|
-
{ value: "", label: lang === "es" ? `Effort por defecto${recommended}` : `Default effort${recommended}` },
|
|
845
|
-
{ value: "high", label: "high" },
|
|
846
|
-
{ value: "max", label: "max" },
|
|
847
|
-
{ value: "custom", label: lang === "es" ? "Effort personalizado" : "Custom effort" },
|
|
848
|
-
],
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
return {
|
|
853
|
-
modelQuestion: ` ¿Qué modelo debe usar ${runtimeClientLabel(client)} para chat y background cuando sea el cliente/backend activo?`,
|
|
854
|
-
modelQuestionEn: ` Which model should ${runtimeClientLabel(client)} use for chat and background when it is the active client/backend?`,
|
|
855
|
-
effortQuestion: ` ¿Qué razonamiento debe usar ${runtimeClientLabel(client)}?`,
|
|
856
|
-
effortQuestionEn: ` Which reasoning effort should ${runtimeClientLabel(client)} use?`,
|
|
857
|
-
customModelQuestion: ` Escribe el nombre del modelo para ${runtimeClientLabel(client)} > `,
|
|
858
|
-
customModelQuestionEn: ` Enter the model name for ${runtimeClientLabel(client)} > `,
|
|
859
|
-
customEffortQuestion: ` Escribe el reasoning effort para ${runtimeClientLabel(client)} > `,
|
|
860
|
-
customEffortQuestionEn: ` Enter the reasoning effort for ${runtimeClientLabel(client)} > `,
|
|
861
|
-
modelDefault: DEFAULT_CODEX_MODEL,
|
|
862
|
-
effortDefault: DEFAULT_CODEX_REASONING_EFFORT,
|
|
863
|
-
modelOptions: [
|
|
864
|
-
{ value: "gpt-5.4", label: `GPT-5.4${recommended}` },
|
|
865
|
-
{ value: "gpt-5.4-pro", label: "GPT-5.4 Pro" },
|
|
866
|
-
{ value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
|
|
867
|
-
{ value: "custom", label: lang === "es" ? "Modelo personalizado" : "Custom model" },
|
|
868
|
-
],
|
|
869
|
-
effortOptions: [
|
|
870
|
-
{ value: "xhigh", label: `xhigh${recommended}` },
|
|
871
|
-
{ value: "high", label: "high" },
|
|
872
|
-
{ value: "medium", label: "medium" },
|
|
873
|
-
{ value: "low", label: "low" },
|
|
874
|
-
{ value: "none", label: "none" },
|
|
875
|
-
{ value: "custom", label: lang === "es" ? "Effort personalizado" : "Custom effort" },
|
|
876
|
-
],
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function askClientRuntimeProfile({ lang, client, currentProfile }) {
|
|
881
|
-
const catalog = runtimeProfileCatalog(lang, client);
|
|
882
|
-
const modelQuestion = lang === "es" ? catalog.modelQuestion : catalog.modelQuestionEn;
|
|
883
|
-
const effortQuestion = lang === "es" ? catalog.effortQuestion : catalog.effortQuestionEn;
|
|
884
|
-
const customModelQuestion = lang === "es" ? catalog.customModelQuestion : catalog.customModelQuestionEn;
|
|
885
|
-
const customEffortQuestion = lang === "es" ? catalog.customEffortQuestion : catalog.customEffortQuestionEn;
|
|
886
|
-
let model = await askChoice(modelQuestion, catalog.modelOptions, currentProfile.model || catalog.modelDefault);
|
|
887
|
-
if (model === "custom") {
|
|
888
|
-
model = (await ask(customModelQuestion)).trim() || catalog.modelDefault;
|
|
889
|
-
}
|
|
890
|
-
let reasoningEffort = await askChoice(
|
|
891
|
-
effortQuestion,
|
|
892
|
-
catalog.effortOptions,
|
|
893
|
-
currentProfile.reasoning_effort ?? catalog.effortDefault,
|
|
894
|
-
);
|
|
895
|
-
if (reasoningEffort === "custom") {
|
|
896
|
-
reasoningEffort = (await ask(customEffortQuestion)).trim();
|
|
897
|
-
}
|
|
898
|
-
return {
|
|
899
|
-
model,
|
|
900
|
-
reasoning_effort: reasoningEffort,
|
|
901
|
-
};
|
|
950
|
+
const question = lang === "es"
|
|
951
|
+
? " ¿Qué nivel de potencia quieres por defecto para tus conversaciones?"
|
|
952
|
+
: " Which default power level do you want for your conversations?";
|
|
953
|
+
const options = [
|
|
954
|
+
{ value: "maximo", label: lang === "es" ? "máximo" : "maximum" },
|
|
955
|
+
{ value: "alto", label: (lang === "es" ? "alto" : "high") + recommended },
|
|
956
|
+
{ value: "medio", label: lang === "es" ? "medio" : "medium" },
|
|
957
|
+
{ value: "bajo", label: lang === "es" ? "bajo" : "low" },
|
|
958
|
+
];
|
|
959
|
+
const fallback = RESONANCE_TIER_NAMES.includes(currentTier) ? currentTier : DEFAULT_RESONANCE_TIER;
|
|
960
|
+
const chosen = await askChoice(question, options, fallback);
|
|
961
|
+
return RESONANCE_TIER_NAMES.includes(chosen) ? chosen : DEFAULT_RESONANCE_TIER;
|
|
902
962
|
}
|
|
903
963
|
|
|
904
964
|
function defaultClientSetup(detected) {
|
|
@@ -1063,28 +1123,13 @@ async function configureClientSetup({ lang, useDefaults, autoInstall, detected }
|
|
|
1063
1123
|
log(strings.desktopManual);
|
|
1064
1124
|
}
|
|
1065
1125
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
].filter(Boolean)));
|
|
1071
|
-
for (const client of activeRuntimeClients) {
|
|
1072
|
-
setup.client_runtime_profiles[client] = await askClientRuntimeProfile({
|
|
1073
|
-
lang,
|
|
1074
|
-
client,
|
|
1075
|
-
currentProfile: setup.client_runtime_profiles[client] || defaultClientRuntimeProfiles()[client] || {},
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1126
|
+
// v6.0.0 — no per-client model/effort prompts. A single tier question
|
|
1127
|
+
// (asked by the main installer flow, not here) will write
|
|
1128
|
+
// preferences.default_resonance into calibration.json. All runtime
|
|
1129
|
+
// resolution then flows through src/resonance_tiers.json.
|
|
1079
1130
|
|
|
1080
|
-
const defaultProfile =
|
|
1081
|
-
|
|
1082
|
-
);
|
|
1083
|
-
const backendProfile = setup.automation_enabled && setup.automation_backend !== "none"
|
|
1084
|
-
? formatRuntimeProfile(
|
|
1085
|
-
setup.client_runtime_profiles[setup.automation_backend] || defaultClientRuntimeProfiles()[setup.automation_backend] || {}
|
|
1086
|
-
)
|
|
1087
|
-
: "";
|
|
1131
|
+
const defaultProfile = "tier";
|
|
1132
|
+
const backendProfile = setup.automation_enabled && setup.automation_backend !== "none" ? "tier" : "";
|
|
1088
1133
|
log(strings.summary(setup.default_terminal_client, defaultProfile, setup.automation_backend, backendProfile, setup.automation_enabled));
|
|
1089
1134
|
return { setup, detected };
|
|
1090
1135
|
}
|
|
@@ -1508,8 +1553,12 @@ WantedBy=timers.target
|
|
|
1508
1553
|
}
|
|
1509
1554
|
|
|
1510
1555
|
async function main() {
|
|
1511
|
-
// Non-interactive mode: --defaults
|
|
1512
|
-
|
|
1556
|
+
// Non-interactive mode: --defaults, --yes, --skip, or -y all skip prompts
|
|
1557
|
+
// and apply the recommended defaults end-to-end (v6.0.0 adds --skip).
|
|
1558
|
+
const useDefaults = process.argv.includes("--defaults")
|
|
1559
|
+
|| process.argv.includes("--yes")
|
|
1560
|
+
|| process.argv.includes("--skip")
|
|
1561
|
+
|| process.argv.includes("-y");
|
|
1513
1562
|
|
|
1514
1563
|
console.log("");
|
|
1515
1564
|
console.log(
|
|
@@ -2158,13 +2207,16 @@ async function main() {
|
|
|
2158
2207
|
console.log("");
|
|
2159
2208
|
}
|
|
2160
2209
|
|
|
2161
|
-
// Step 2: User's name (P2)
|
|
2162
|
-
|
|
2210
|
+
// Step 2: User's name (P2) — v6.0.0 empty input falls through to "Usuario"
|
|
2211
|
+
// instead of keeping an empty string. The calibration file always ships
|
|
2212
|
+
// with a concrete user.name so downstream tooling does not need guards.
|
|
2213
|
+
let userName = "Usuario";
|
|
2163
2214
|
if (!useDefaults) {
|
|
2164
2215
|
const nameInput = await ask(t.askUserName);
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2216
|
+
const trimmedName = nameInput.trim();
|
|
2217
|
+
userName = trimmedName || "Usuario";
|
|
2218
|
+
if (trimmedName) {
|
|
2219
|
+
log(t.userGreet(trimmedName));
|
|
2168
2220
|
console.log("");
|
|
2169
2221
|
}
|
|
2170
2222
|
}
|
|
@@ -2175,6 +2227,17 @@ async function main() {
|
|
|
2175
2227
|
log(t.agentConfirm(operatorName));
|
|
2176
2228
|
console.log("");
|
|
2177
2229
|
|
|
2230
|
+
// Step 3b (v6.0.0): Resonance tier — the ONE power-level question. Drives
|
|
2231
|
+
// every runtime call for both Claude Code and Codex via resonance_tiers.json.
|
|
2232
|
+
let resonanceTier = DEFAULT_RESONANCE_TIER;
|
|
2233
|
+
if (!useDefaults) {
|
|
2234
|
+
resonanceTier = await askResonanceTier(lang, DEFAULT_RESONANCE_TIER);
|
|
2235
|
+
log(lang === "es"
|
|
2236
|
+
? `Potencia por defecto: ${resonanceTier}.`
|
|
2237
|
+
: `Default power: ${resonanceTier}.`);
|
|
2238
|
+
console.log("");
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2178
2241
|
// Step 4: Personality Calibration (P4-P8)
|
|
2179
2242
|
let autonomyLevel = "full", communicationStyle = "concise", honestyLevel = "firm-pushback", proactivityLevel = "proactive", errorHandling = "brief-fix";
|
|
2180
2243
|
|
|
@@ -2203,16 +2266,31 @@ async function main() {
|
|
|
2203
2266
|
log(`Calibrated: autonomy=${autonomyLevel}, communication=${communicationStyle}, honesty=${honestyLevel}, proactivity=${proactivityLevel}, errors=${errorHandling}`);
|
|
2204
2267
|
console.log("");
|
|
2205
2268
|
|
|
2206
|
-
// Save calibration
|
|
2269
|
+
// Save calibration (v6.0.0 — canonical nested shape with
|
|
2270
|
+
// preferences.default_resonance as the one knob for tier-only setup).
|
|
2207
2271
|
const calibration = {
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2272
|
+
version: 1,
|
|
2273
|
+
created: new Date().toISOString().slice(0, 10),
|
|
2274
|
+
user: {
|
|
2275
|
+
name: userName,
|
|
2276
|
+
language: lang,
|
|
2277
|
+
assistant_name: operatorName,
|
|
2278
|
+
},
|
|
2279
|
+
personality: {
|
|
2280
|
+
autonomy: autonomyLevel,
|
|
2281
|
+
communication: communicationStyle,
|
|
2282
|
+
honesty: honestyLevel,
|
|
2283
|
+
proactivity: proactivityLevel,
|
|
2284
|
+
error_handling: errorHandling,
|
|
2285
|
+
},
|
|
2286
|
+
preferences: {
|
|
2287
|
+
menu_on_demand: true,
|
|
2288
|
+
default_resonance: resonanceTier,
|
|
2289
|
+
report_style: "essentials_only",
|
|
2290
|
+
execution_first: true,
|
|
2291
|
+
},
|
|
2292
|
+
meta: {},
|
|
2293
|
+
auto_install: "ask", // updated later if user answers P11
|
|
2216
2294
|
calibrated_at: new Date().toISOString(),
|
|
2217
2295
|
};
|
|
2218
2296
|
// Ensure NEXO_HOME and brain dir exist before writing calibration
|
|
@@ -2223,41 +2301,61 @@ async function main() {
|
|
|
2223
2301
|
JSON.stringify(calibration, null, 2)
|
|
2224
2302
|
);
|
|
2225
2303
|
|
|
2226
|
-
// Step 5: Deep scan (P9)
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
let
|
|
2304
|
+
// Step 5: Deep scan (P9) — v6.0.0 defaults flip to ON when running in
|
|
2305
|
+
// --yes/--skip mode; the interactive prompt below defaults to "yes" too
|
|
2306
|
+
// so a bare ENTER keeps the recommended setup.
|
|
2307
|
+
let doScan = useDefaults;
|
|
2308
|
+
let doCaffeinate = useDefaults && platform === "darwin";
|
|
2309
|
+
let doDashboard = useDefaults;
|
|
2230
2310
|
let autoInstall = useDefaults ? "auto" : "ask";
|
|
2311
|
+
// v6.0.0 — bare ENTER on each of these prompts is interpreted as "yes"
|
|
2312
|
+
// because the recommended defaults are all on. An explicit "2" or "n"
|
|
2313
|
+
// turns the feature off.
|
|
2314
|
+
const answerIsYesDefault = (answer) => {
|
|
2315
|
+
const trimmed = String(answer || "").trim().toLowerCase();
|
|
2316
|
+
if (!trimmed) return true;
|
|
2317
|
+
if (trimmed === "1" || trimmed.startsWith("y") || trimmed.startsWith("s")) return true;
|
|
2318
|
+
return false;
|
|
2319
|
+
};
|
|
2231
2320
|
if (!useDefaults) {
|
|
2232
2321
|
const scanAnswer = await ask(t.scanQ);
|
|
2233
|
-
doScan =
|
|
2322
|
+
doScan = answerIsYesDefault(scanAnswer);
|
|
2234
2323
|
console.log("");
|
|
2235
2324
|
|
|
2236
2325
|
// Step 6: Caffeinate (P10) — macOS only
|
|
2237
2326
|
if (platform === "darwin") {
|
|
2238
2327
|
const caffeinateAnswer = await ask(t.caffeinateQ);
|
|
2239
|
-
doCaffeinate =
|
|
2328
|
+
doCaffeinate = answerIsYesDefault(caffeinateAnswer);
|
|
2240
2329
|
log(doCaffeinate ? `✓ ${t.caffYes}` : t.caffNo);
|
|
2241
2330
|
console.log("");
|
|
2242
2331
|
}
|
|
2243
2332
|
|
|
2244
2333
|
// Step 6b: Dashboard — always-on web UI
|
|
2245
2334
|
const dashAnswer = await ask(t.dashboardQ);
|
|
2246
|
-
doDashboard =
|
|
2335
|
+
doDashboard = answerIsYesDefault(dashAnswer);
|
|
2247
2336
|
log(doDashboard ? `✓ ${t.dashYes}` : t.dashNo);
|
|
2248
2337
|
console.log("");
|
|
2249
2338
|
|
|
2250
2339
|
// Step 7: Auto-install permission (P11)
|
|
2251
2340
|
const autoInstallAnswer = await ask(t.autoInstallQ);
|
|
2252
|
-
autoInstall = (autoInstallAnswer
|
|
2341
|
+
autoInstall = answerIsYesDefault(autoInstallAnswer) ? "auto" : "ask";
|
|
2253
2342
|
calibration.auto_install = autoInstall;
|
|
2254
2343
|
log(`✓ ${autoInstall === "auto" ? t.autoInstallYes : t.autoInstallNo}`);
|
|
2255
2344
|
console.log("");
|
|
2256
2345
|
} else {
|
|
2257
|
-
log("Skipping interactive setup (non-interactive mode).");
|
|
2346
|
+
log("Skipping interactive setup (non-interactive mode, defaults applied).");
|
|
2347
|
+
calibration.auto_install = autoInstall;
|
|
2258
2348
|
console.log("");
|
|
2259
2349
|
}
|
|
2260
2350
|
|
|
2351
|
+
// Persist the updated calibration (auto_install may have changed post-write above).
|
|
2352
|
+
try {
|
|
2353
|
+
fs.writeFileSync(
|
|
2354
|
+
path.join(NEXO_HOME, "brain", "calibration.json"),
|
|
2355
|
+
JSON.stringify(calibration, null, 2)
|
|
2356
|
+
);
|
|
2357
|
+
} catch (_) {}
|
|
2358
|
+
|
|
2261
2359
|
const clientConfig = await configureClientSetup({
|
|
2262
2360
|
lang,
|
|
2263
2361
|
useDefaults,
|