ruvnet-brain 3.9.68-dev โ 3.9.72-dev
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/README.md +1 -1
- package/bin/install.mjs +301 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# ๐ง RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### ๐ง RuvNet Brain โ [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
8
8
|
|
|
9
9
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack โ delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
10
10
|
|
package/bin/install.mjs
CHANGED
|
@@ -80,6 +80,7 @@ const FLAG_UNINSTALL = argv.includes('--uninstall'); // reverse everything, in o
|
|
|
80
80
|
const FLAG_WHAT_CHANGED = argv.includes('--what-changed'); // show our footprint on this machine
|
|
81
81
|
// โโ onboarding-experience flags (all optional; every offer is safe to decline) โโ
|
|
82
82
|
const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
|
|
83
|
+
const FLAG_PLAN = argv.includes('--plan') || argv.includes('--dry-run'); // show the interactive checklist, then exit โ install NOTHING
|
|
83
84
|
const FLAG_WITH_STACK = argv.includes('--with-stack'); // add missing Ruflo/RuVector without prompting
|
|
84
85
|
const FLAG_NO_STACK = argv.includes('--no-stack'); // skip the toolkit offer entirely
|
|
85
86
|
const FLAG_ENHANCE_CLAUDE_MD = argv.includes('--enhance-claude-md'); // add the CLAUDE.md section without prompting
|
|
@@ -543,6 +544,121 @@ function wirePlugin() {
|
|
|
543
544
|
return { wired: false, manualMarketplace, manualInstall };
|
|
544
545
|
}
|
|
545
546
|
|
|
547
|
+
// โโ step: wire the Codex host (issue #42, Henrik Pettersen) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
548
|
+
// We shipped a .codex/ directory AND a working MCP server and never wired them together, so on a
|
|
549
|
+
// Codex host the brain was entirely absent โ no search_ruvnet at all. Every artifact was present;
|
|
550
|
+
// only the registration was missing. Codex reads its MCP servers from ~/.codex/config.toml, which is
|
|
551
|
+
// the USER's file and already carries their own settings, so this MERGES and never rewrites wholesale.
|
|
552
|
+
//
|
|
553
|
+
// Two rules make that merge safe:
|
|
554
|
+
// 1. Our lines live inside a comment-delimited managed block, so a reinstall rewrites exactly
|
|
555
|
+
// those bytes and leaves every other section byte-identical.
|
|
556
|
+
// 2. An [mcp_servers.ruvnet-brain] found OUTSIDE our markers is the user's, not ours โ we report
|
|
557
|
+
// it and touch nothing. Their hand-written config outranks our convenience.
|
|
558
|
+
//
|
|
559
|
+
// The registered path must also OUTLIVE the install. The npx checkout is ephemeral (the same reason
|
|
560
|
+
// the spend watchdog and the router tools get copied under ~/.claude), and plugin/mcp/server.mjs is
|
|
561
|
+
// self-contained โ node builtins only โ so it is copied to a persistent home and THAT absolute path
|
|
562
|
+
// is what gets registered. Registering the npx dir would rot the moment the temp dir vanished.
|
|
563
|
+
const CODEX_BLOCK_START = '# --- ruvnet-brain (managed block, installer-rewritten) ---';
|
|
564
|
+
const CODEX_BLOCK_END = '# --- end ruvnet-brain ---';
|
|
565
|
+
const codexHomeDir = () => path.join(os.homedir(), '.codex');
|
|
566
|
+
const codexConfigPath = () => path.join(codexHomeDir(), 'config.toml');
|
|
567
|
+
const codexServerDir = () => path.join(os.homedir(), '.claude', 'ruvnet-brain', 'mcp');
|
|
568
|
+
|
|
569
|
+
// The exact bytes we own. Kept in one place so the writer and the doctor probe can never disagree.
|
|
570
|
+
function codexManagedBlock(serverPath) {
|
|
571
|
+
return [
|
|
572
|
+
CODEX_BLOCK_START,
|
|
573
|
+
'# Written by `npx ruvnet-brain`. This block is rewritten on every reinstall;',
|
|
574
|
+
'# everything outside the two markers is yours and is never touched.',
|
|
575
|
+
'[mcp_servers.ruvnet-brain]',
|
|
576
|
+
'command = "node"',
|
|
577
|
+
`args = [${JSON.stringify(serverPath)}]`,
|
|
578
|
+
CODEX_BLOCK_END,
|
|
579
|
+
].join('\n');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Pure, so the merge contract is testable without going near a real ~/.codex. Returns the new text
|
|
583
|
+
// plus which of the three things happened, and is idempotent: re-running over its own output
|
|
584
|
+
// reproduces it byte for byte.
|
|
585
|
+
export function mergeCodexConfig(existing, serverPath) {
|
|
586
|
+
const text = typeof existing === 'string' ? existing : '';
|
|
587
|
+
const block = codexManagedBlock(serverPath);
|
|
588
|
+
const start = text.indexOf(CODEX_BLOCK_START);
|
|
589
|
+
const end = text.indexOf(CODEX_BLOCK_END);
|
|
590
|
+
if (start !== -1 && end > start) {
|
|
591
|
+
return { text: text.slice(0, start) + block + text.slice(end + CODEX_BLOCK_END.length), action: 'rewritten' };
|
|
592
|
+
}
|
|
593
|
+
// Their own declaration, outside our markers. Never clobber a user's hand-written server entry.
|
|
594
|
+
if (/^[ \t]*\[mcp_servers\.(?:ruvnet-brain|"ruvnet-brain"|'ruvnet-brain')\]/m.test(text)) {
|
|
595
|
+
return { text, action: 'user-owned' };
|
|
596
|
+
}
|
|
597
|
+
const sep = text.length === 0 ? '' : text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
|
|
598
|
+
return { text: `${text}${sep}${block}\n`, action: 'added' };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// What the doctor asserts from disk โ never from the fact that we once ran. "Wired" means our entry
|
|
602
|
+
// is in the config AND the server.mjs it points at is really there, because a registration pointing
|
|
603
|
+
// at a deleted file is worse than no registration: Codex fails at spawn time with nothing to read.
|
|
604
|
+
export function codexStatus({ configPath = codexConfigPath(), codexDir = codexHomeDir() } = {}) {
|
|
605
|
+
let host = false;
|
|
606
|
+
try { host = fs.existsSync(codexDir); } catch { /* unreadable โ treat as absent */ }
|
|
607
|
+
if (!host) return { host: false, wired: false, serverPath: null, serverExists: false };
|
|
608
|
+
let text = '';
|
|
609
|
+
try { text = fs.readFileSync(configPath, 'utf8'); } catch { /* no config yet */ }
|
|
610
|
+
const m = /^[ \t]*\[mcp_servers\.(?:ruvnet-brain|"ruvnet-brain"|'ruvnet-brain')\][^[]*?args\s*=\s*\[\s*"([^"]+)"/m.exec(text);
|
|
611
|
+
const serverPath = m ? m[1] : null;
|
|
612
|
+
let serverExists = false;
|
|
613
|
+
try { serverExists = serverPath ? fs.existsSync(serverPath) : false; } catch { /* unreadable */ }
|
|
614
|
+
return { host: true, wired: Boolean(serverPath) && serverExists, serverPath, serverExists };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function wireCodexHost({
|
|
618
|
+
codexDir = codexHomeDir(),
|
|
619
|
+
configPath = path.join(codexDir, 'config.toml'),
|
|
620
|
+
serverDir = codexServerDir(),
|
|
621
|
+
source = path.join(__dirname, '..', 'plugin', 'mcp', 'server.mjs'),
|
|
622
|
+
announce = true,
|
|
623
|
+
} = {}) {
|
|
624
|
+
let host = false;
|
|
625
|
+
try { host = fs.existsSync(codexDir); } catch { /* unreadable โ treat as absent */ }
|
|
626
|
+
// No Codex on this machine is not a warning. Say nothing and change nothing.
|
|
627
|
+
if (!host) return { host: false, action: 'no-host' };
|
|
628
|
+
|
|
629
|
+
if (announce) {
|
|
630
|
+
step('Wiring the Codex host', 'Codex reads MCP servers from ~/.codex/config.toml โ the brain was never registered there');
|
|
631
|
+
}
|
|
632
|
+
if (!fs.existsSync(source)) {
|
|
633
|
+
if (announce) warn('MCP server missing from this bundle โ Codex left untouched (non-fatal)');
|
|
634
|
+
return { host: true, action: 'no-source' };
|
|
635
|
+
}
|
|
636
|
+
const serverPath = path.join(serverDir, 'server.mjs');
|
|
637
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
638
|
+
fs.copyFileSync(source, serverPath);
|
|
639
|
+
|
|
640
|
+
let before = '';
|
|
641
|
+
try { before = fs.readFileSync(configPath, 'utf8'); } catch { /* first run โ no config yet */ }
|
|
642
|
+
const { text, action } = mergeCodexConfig(before, serverPath);
|
|
643
|
+
if (action === 'user-owned') {
|
|
644
|
+
if (announce) {
|
|
645
|
+
ok('Codex already declares ruvnet-brain in your own config โ left exactly as you wrote it');
|
|
646
|
+
info(` to hand it to us instead, delete that ${c.bold('[mcp_servers.ruvnet-brain]')} block and re-run this installer`);
|
|
647
|
+
}
|
|
648
|
+
return { host: true, action, serverPath };
|
|
649
|
+
}
|
|
650
|
+
if (text !== before) {
|
|
651
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
652
|
+
fs.writeFileSync(configPath, text);
|
|
653
|
+
}
|
|
654
|
+
if (announce) {
|
|
655
|
+
ok(`search_ruvnet registered for Codex โ ${c.bold(configPath)}`);
|
|
656
|
+
info(` server: ${serverPath} ${c.dim('(persistent copy โ the npx dir vanishes)')}`);
|
|
657
|
+
info(` ${c.dim('only our marked block is written; every other section is byte-preserved')}`);
|
|
658
|
+
}
|
|
659
|
+
return { host: true, action, serverPath, changed: text !== before };
|
|
660
|
+
}
|
|
661
|
+
|
|
546
662
|
// โโ step: verify the install is REAL (counts โ never take "installed" on faith) โโโโโโโโโโโโโโโโโโ
|
|
547
663
|
// Shared state-gatherer behind verifyInstall / --doctor / --feedback: what is REALLY on disk.
|
|
548
664
|
// Pure read, never prints โ callers decide how to narrate (or, for --feedback, how to report) it.
|
|
@@ -865,13 +981,14 @@ async function doctor() {
|
|
|
865
981
|
have('git')
|
|
866
982
|
? ok('git present (not required by this installer, but handy)')
|
|
867
983
|
: info('git not found โ that\'s fine, this installer never needs it');
|
|
984
|
+
cleanupStrayRuvectorDb(); // issue #39: sweep a leftover empty scaffold from before this fix, if one's here
|
|
868
985
|
const env = detectEnvironment();
|
|
869
986
|
env.ruflo
|
|
870
987
|
? ok('Ruflo present โ orchestration / swarms / SPARC available')
|
|
871
988
|
: warn('Ruflo not found โ answers still work. To build: npm install -g claude-flow@alpha (or /plugin add ruvnet/claude-flow)');
|
|
872
989
|
env.ruvector
|
|
873
990
|
? ok('RuVector present โ vector CLI / MCP available')
|
|
874
|
-
: warn(
|
|
991
|
+
: warn(`RuVector not found โ answers still work. To add: ${buildRuvectorMcpAddCommand(env).say}`);
|
|
875
992
|
// Network probe (issue #27, Jan Lafko): in a network-restricted sandbox the cold-cache embedder
|
|
876
993
|
// pull used to hang FOREVER. Diagnose the condition here, explicitly and in 3 seconds flat: if the
|
|
877
994
|
// model host is unreachable AND no local model cache exists, the first query needs the network and
|
|
@@ -911,11 +1028,34 @@ async function doctor() {
|
|
|
911
1028
|
console.log(` ${c.yellow('! Grounding not verifiable')} on this bundle โ it predates the citation verifier.`);
|
|
912
1029
|
console.log(' Re-run npx ruvnet-brain to refresh, then --doctor will prove it.');
|
|
913
1030
|
}
|
|
1031
|
+
// Codex is a SECOND host, and until issue #42 the doctor was silent about it โ so a machine where
|
|
1032
|
+
// the brain was completely unavailable in Codex still read "Healthy โฆ Grounding PROVEN". Probed
|
|
1033
|
+
// from disk (our entry present AND the server.mjs it names really there), never asserted from the
|
|
1034
|
+
// fact that an install once ran.
|
|
1035
|
+
const cx = codexStatus();
|
|
1036
|
+
if (!cx.host) {
|
|
1037
|
+
console.log(` ${c.dim('Codex: no host detected (no ~/.codex) โ nothing to wire.')}`);
|
|
1038
|
+
} else if (cx.wired) {
|
|
1039
|
+
console.log(` ${c.green('โ Codex: wired.')} search_ruvnet is registered in ~/.codex/config.toml and its server exists.`);
|
|
1040
|
+
} else {
|
|
1041
|
+
console.log(` ${c.yellow('! Codex: host detected but NOT wired')}${
|
|
1042
|
+
cx.serverPath && !cx.serverExists ? ` โ registered server is missing (${cx.serverPath})` : ' โ no [mcp_servers.ruvnet-brain] entry'
|
|
1043
|
+
}.`);
|
|
1044
|
+
console.log(` Codex cannot reach the brain until it is. Fix: re-run ${c.bold('npx ruvnet-brain')}`);
|
|
1045
|
+
}
|
|
914
1046
|
console.log(` ${c.dim(meterSummaryLine())}`);
|
|
915
1047
|
if (allGreen) {
|
|
916
1048
|
console.log(`\n ${c.bold('What this means for you:')}`);
|
|
917
|
-
|
|
918
|
-
|
|
1049
|
+
// "EVERY project" is a claim about Claude Code. On a machine with an unwired Codex host it would
|
|
1050
|
+
// be read as "every editor", which is exactly the invisible gap issue #42 reported โ so scope the
|
|
1051
|
+
// sentence honestly instead of quietly overclaiming.
|
|
1052
|
+
if (cx.host && !cx.wired) {
|
|
1053
|
+
console.log(` โข ${c.bold('It works in EVERY project in Claude Code')} โ user-level (global). Open Claude Code in any`);
|
|
1054
|
+
console.log(` repo or VS Code window and it's there. ${c.bold('No reinstall per project.')} ${c.yellow('Codex is NOT wired yet')} (above).`);
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log(` โข ${c.bold('It works in EVERY project')} โ user-level (global). Open Claude Code in any repo or VS Code`);
|
|
1057
|
+
console.log(` window and it's there. ${c.bold('No reinstall per project. No second download.')} One brain, shared.`);
|
|
1058
|
+
}
|
|
919
1059
|
console.log(` โข ${c.bold('Nothing to git-ignore')} in your projects โ it drops zero files into your working repos.`);
|
|
920
1060
|
console.log(` โข ${c.bold('To use it:')} just ask Claude about rUv's stack (RuVector, Ruflo, AgentDB, SPARCโฆ) โ it`);
|
|
921
1061
|
console.log(` grounds the answer automatically and takes the lead on builds. You don't invoke anything.`);
|
|
@@ -1682,7 +1822,10 @@ export async function offerSpendGuard() {
|
|
|
1682
1822
|
// NOT gated on FLAG_YES โ second launchd job, same rule as the nightly updater above.
|
|
1683
1823
|
if (!process.stdin.isTTY && !FLAG_ENABLE_SPEND_GUARD) { info(`No terminal to prompt on โ install it any time with ${c.bold('npx ruvnet-brain --enable-spend-guard')}`); return 'recommended'; }
|
|
1684
1824
|
let yes = true;
|
|
1685
|
-
|
|
1825
|
+
const plannedSpend = plannedChoice('spend');
|
|
1826
|
+
if (plannedSpend !== undefined) {
|
|
1827
|
+
yes = plannedSpend; // answered in the up-front checklist โ never re-ask
|
|
1828
|
+
} else if (!FLAG_ENABLE_SPEND_GUARD) {
|
|
1686
1829
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1687
1830
|
const answer = await new Promise((resolve) => rl.question(` ${c.cyan('?')} Install the spend watchdog? ${c.dim('[Y/n]')} `, resolve));
|
|
1688
1831
|
rl.close();
|
|
@@ -1848,7 +1991,11 @@ export async function offerNightly() {
|
|
|
1848
1991
|
}
|
|
1849
1992
|
|
|
1850
1993
|
let yes = true;
|
|
1851
|
-
|
|
1994
|
+
const planned = plannedChoice('nightly');
|
|
1995
|
+
if (planned !== undefined) {
|
|
1996
|
+
// Already answered in the up-front checklist โ don't ask twice (the owner's complaint).
|
|
1997
|
+
yes = planned;
|
|
1998
|
+
} else if (!FLAG_ENABLE_NIGHTLY) {
|
|
1852
1999
|
// Not ask(): its parser treats anything but y/yes as no. Here the DEFAULT is yes โ only an
|
|
1853
2000
|
// explicit n/no declines (parseNightlyAnswer holds that contract, and the tests hold it there).
|
|
1854
2001
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -1925,7 +2072,10 @@ export async function offerTelemetry(cacheDir) {
|
|
|
1925
2072
|
}
|
|
1926
2073
|
|
|
1927
2074
|
let yes = true; // --yes accepts every optional offer, this one included
|
|
1928
|
-
|
|
2075
|
+
const plannedTelemetry = plannedChoice('telemetry');
|
|
2076
|
+
if (plannedTelemetry !== undefined) {
|
|
2077
|
+
yes = plannedTelemetry; // answered in the up-front checklist โ never re-ask
|
|
2078
|
+
} else if (!FLAG_YES) {
|
|
1929
2079
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1930
2080
|
const answer = await new Promise((resolve) =>
|
|
1931
2081
|
rl.question(` ${c.cyan('?')} Share anonymous usage counts (installs/searches โ never your queries or code)? ${c.dim('[Y/n]')} `, resolve),
|
|
@@ -2004,66 +2154,96 @@ async function printPlanAndConfirm() {
|
|
|
2004
2154
|
const H = os.homedir();
|
|
2005
2155
|
const short = (p) => p.replace(H, '~');
|
|
2006
2156
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2157
|
+
// The REQUIRED core โ always runs, shown for transparency, never a question (nothing works without
|
|
2158
|
+
// these, so asking would be theatre).
|
|
2159
|
+
const core = [
|
|
2160
|
+
{ name: 'Download the knowledge base',
|
|
2010
2161
|
cost: short(resolveCacheDir().cacheDir),
|
|
2011
|
-
what: 'Real rUv source, on your disk, so answers cite files instead of guessing.',
|
|
2012
|
-
|
|
2013
|
-
{ auto: true, name: 'Register the Claude Code plugin + MCP server',
|
|
2162
|
+
what: 'Real rUv source, on your disk, so answers cite files instead of guessing.' },
|
|
2163
|
+
{ name: 'Register the Claude Code plugin + MCP server',
|
|
2014
2164
|
cost: `an entry in ${short(path.join(H, '.claude'))}`,
|
|
2015
|
-
what: 'Gives Claude a search_ruvnet tool. Adds no hooks you have not agreed to.',
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
{
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
{ ask: true, name: 'Spend watchdog',
|
|
2026
|
-
cost: 'one LaunchAgent',
|
|
2165
|
+
what: 'Gives Claude a search_ruvnet tool. Adds no hooks you have not agreed to.' },
|
|
2166
|
+
];
|
|
2167
|
+
// The OPTIONAL steps โ each asked ONCE here, stored in PLAN_CHOICES, and consumed by its offer
|
|
2168
|
+
// later (no second prompt). `def` is the pre-check: nightly + spend are recommended (default yes),
|
|
2169
|
+
// the rest default off. `rec` tags the two recommended ones for the display grouping.
|
|
2170
|
+
const optional = [
|
|
2171
|
+
{ key: 'nightly', rec: true, name: 'Evergreen auto-updates',
|
|
2172
|
+
what: 'Keeps the KB and plugin current โ declining makes updates manual forever. rUv ships fast, and a stale brain is the main way this stops being useful.',
|
|
2173
|
+
undo: 'npx ruvnet-brain --disable-nightly', def: true },
|
|
2174
|
+
{ key: 'spend', rec: true, name: 'Spend watchdog',
|
|
2027
2175
|
what: 'Warns you if an agent fleet starts burning API credit unexpectedly.',
|
|
2028
|
-
undo: 'npx ruvnet-brain --disable-spend-guard' },
|
|
2029
|
-
{
|
|
2030
|
-
|
|
2176
|
+
undo: 'npx ruvnet-brain --disable-spend-guard', def: true },
|
|
2177
|
+
{ key: 'stack', name: 'Add rUv tools you are missing',
|
|
2178
|
+
what: 'So the brain can build with them, not just answer questions about them. The brain answers grounded questions fine without them.',
|
|
2179
|
+
undo: 'npm uninstall -g <tool>', def: false },
|
|
2180
|
+
{ key: 'statusline', name: 'Status-bar version segment',
|
|
2031
2181
|
what: 'Shows which brain version is live while you work.',
|
|
2032
|
-
undo: 'npx ruvnet-brain --no-statusline,
|
|
2033
|
-
{
|
|
2034
|
-
cost: 'a counter ping',
|
|
2182
|
+
undo: 'npx ruvnet-brain --no-statusline', def: false },
|
|
2183
|
+
{ key: 'telemetry', name: 'Anonymous usage counts',
|
|
2035
2184
|
what: 'Installs and searches only โ never your queries, your code, or your paths.',
|
|
2036
|
-
undo: 'npx ruvnet-brain --no-telemetry' },
|
|
2185
|
+
undo: 'npx ruvnet-brain --no-telemetry', def: true },
|
|
2037
2186
|
];
|
|
2038
2187
|
|
|
2039
2188
|
console.log(` ${c.bold('Here is everything this will do.')} Nothing has happened yet.\n`);
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
console.log(`
|
|
2043
|
-
console.log(`
|
|
2044
|
-
console.log(` ${c.dim('undo: ' + s.undo)}\n`);
|
|
2189
|
+
console.log(` ${c.bold('The core')} ${c.dim('โ required; happens automatically')}`);
|
|
2190
|
+
for (const s of core) {
|
|
2191
|
+
console.log(` ${c.green('โ')} ${c.bold(s.name)} ${c.dim('ยท ' + s.cost)}`);
|
|
2192
|
+
console.log(` ${s.what}`);
|
|
2045
2193
|
}
|
|
2046
|
-
console.log(
|
|
2047
|
-
console.log(` ${c.dim('Every step is reversible, and `npx ruvnet-brain --what-changed` lists everything it touched.')}\n`);
|
|
2194
|
+
console.log('');
|
|
2048
2195
|
|
|
2049
|
-
// FLAG_YES
|
|
2050
|
-
//
|
|
2051
|
-
// the pause is a courtesy to humans.
|
|
2196
|
+
// FLAG_YES / non-TTY: the caller already decided (agentic-kit, scripted installs). Print the plan,
|
|
2197
|
+
// leave PLAN_CHOICES empty, and let each offer honor its own flags/defaults as before. Never block.
|
|
2052
2198
|
if (FLAG_YES || FLAG_AUTO || !process.stdin.isTTY) {
|
|
2199
|
+
console.log(` ${c.dim('The rest (auto-update, spend watchdog, extras) is handled non-interactively by each step.')}`);
|
|
2200
|
+
if (FLAG_PLAN) {
|
|
2201
|
+
console.log(` ${c.dim('--plan: preview only. Nothing was installed.')}\n`);
|
|
2202
|
+
process.exit(0);
|
|
2203
|
+
}
|
|
2053
2204
|
console.log(c.dim(' (non-interactive โ continuing)\n'));
|
|
2054
2205
|
return;
|
|
2055
2206
|
}
|
|
2207
|
+
|
|
2208
|
+
// INTERACTIVE CHECKLIST โ decide each optional ONCE, here, instead of being re-asked one by one.
|
|
2209
|
+
console.log(` ${c.bold('Choose the optional pieces')} ${c.dim('โ press Enter to accept the [default]; every one is reversible')}\n`);
|
|
2056
2210
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2057
|
-
const
|
|
2211
|
+
const askOne = (q, def) => new Promise((resolve) => {
|
|
2212
|
+
const suffix = def ? c.dim('[Y/n]') : c.dim('[y/N]');
|
|
2213
|
+
rl.question(` ${def ? c.yellow('โ') : c.dim('โ')} ${c.bold(q)} ${suffix} `, (a) => {
|
|
2214
|
+
const s = String(a).trim().toLowerCase();
|
|
2215
|
+
resolve(s === '' ? def : (s === 'y' || s === 'yes'));
|
|
2216
|
+
});
|
|
2217
|
+
});
|
|
2218
|
+
for (const s of optional) {
|
|
2219
|
+
console.log(` ${s.what}`);
|
|
2220
|
+
console.log(` ${c.dim('undo: ' + s.undo)}`);
|
|
2221
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2222
|
+
PLAN_CHOICES[s.key] = await askOne(s.name + (s.rec ? c.dim(' (recommended)') : ''), s.def);
|
|
2223
|
+
console.log('');
|
|
2224
|
+
}
|
|
2058
2225
|
rl.close();
|
|
2059
|
-
|
|
2060
|
-
|
|
2226
|
+
|
|
2227
|
+
const on = optional.filter((s) => PLAN_CHOICES[s.key]).map((s) => s.name);
|
|
2228
|
+
console.log(` ${c.green('โ')} Plan set โ the core, plus: ${on.length ? c.bold(on.join(', ')) : c.dim('none of the optional pieces')}.`);
|
|
2229
|
+
if (FLAG_PLAN) {
|
|
2230
|
+
console.log(` ${c.dim('--plan: preview only. Nothing was installed. Re-run without --plan to apply this.')}\n`);
|
|
2061
2231
|
process.exit(0);
|
|
2062
2232
|
}
|
|
2063
|
-
console.log('');
|
|
2233
|
+
console.log(` ${c.dim('`npx ruvnet-brain --what-changed` lists everything it touched. Installingโฆ')}\n`);
|
|
2064
2234
|
}
|
|
2065
2235
|
|
|
2066
|
-
|
|
2236
|
+
// THE CHECKLIST'S ANSWERS, collected ONCE up front in printPlanAndConfirm() and consumed by each
|
|
2237
|
+
// offer instead of a second serial prompt. The owner's complaint (2026-07-24): the plan screen
|
|
2238
|
+
// showed everything, then every offer asked again, one at a time. Now you decide once. Empty in the
|
|
2239
|
+
// non-interactive / --yes / CI paths (printPlanAndConfirm returns before populating it), so every
|
|
2240
|
+
// offer falls through to its existing flag/default behavior unchanged.
|
|
2241
|
+
export const PLAN_CHOICES = Object.create(null);
|
|
2242
|
+
/** The checklist's answer for `key`, or undefined if it wasn't collected (so the offer prompts as before). */
|
|
2243
|
+
export function plannedChoice(key) { return key in PLAN_CHOICES ? PLAN_CHOICES[key] : undefined; }
|
|
2244
|
+
|
|
2245
|
+
export function ask(question, def = false, { blanketYes = true, planKey = null } = {}) {
|
|
2246
|
+
if (planKey != null) { const p = plannedChoice(planKey); if (p !== undefined) return Promise.resolve(p); }
|
|
2067
2247
|
if (FLAG_YES && blanketYes) return Promise.resolve(true);
|
|
2068
2248
|
if (!process.stdin.isTTY) return Promise.resolve(def);
|
|
2069
2249
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -2116,12 +2296,79 @@ function detectEnvironment() {
|
|
|
2116
2296
|
return env;
|
|
2117
2297
|
}
|
|
2118
2298
|
|
|
2299
|
+
// โโ issue #39: the ruvector MCP server's own native VectorDb defaults ITS storage to
|
|
2300
|
+
// "./ruvector.db" relative to whatever cwd it happens to be launched in โ and Claude Code
|
|
2301
|
+
// always launches an MCP server with cwd = the current project, so every consumer project got
|
|
2302
|
+
// an empty ~1.5MB scaffold dropped in its own root. Traced live (2026-07-24) to
|
|
2303
|
+
// ruvector/dist/core/intelligence-engine.js `initVectorDb()`, which constructs the native
|
|
2304
|
+
// VectorDb with `{ dimensions, distanceMetric: 'Cosine' }` and NO storagePath at all โ that
|
|
2305
|
+
// constructor runs unconditionally from a module-level `new Intelligence()` the instant
|
|
2306
|
+
// `ruvector mcp start` boots. `claude mcp add` has no --cwd flag, and `ruvector mcp start
|
|
2307
|
+
// --help` documents only `-h` (verified live) โ no flag or env var lets us set the storage
|
|
2308
|
+
// path directly. So the fix has to live in the command we REGISTER: a tiny `node -e` launcher
|
|
2309
|
+
// that uses child_process's own `cwd` option (no shell, so it behaves the same on Windows) to
|
|
2310
|
+
// relocate into a fixed cache dir before delegating to the real `npx -y ruvector mcp start`.
|
|
2311
|
+
// Verified live: this reproduces the exact same empty scaffold, just under
|
|
2312
|
+
// ~/.cache/ruvnet-brain/ruvector-mcp instead of whatever project happens to be open.
|
|
2313
|
+
const RUVECTOR_MCP_CWD = path.join(os.homedir(), '.cache', 'ruvnet-brain', 'ruvector-mcp');
|
|
2314
|
+
function buildRuvectorMcpLauncher(cwd) {
|
|
2315
|
+
return (
|
|
2316
|
+
'const fs=require("fs"),os=require("os"),path=require("path"),cp=require("child_process");' +
|
|
2317
|
+
`const d=${JSON.stringify(cwd)};` +
|
|
2318
|
+
'fs.mkdirSync(d,{recursive:true});' +
|
|
2319
|
+
'const r=cp.spawnSync("npx",["-y","ruvector","mcp","start"],{stdio:"inherit",cwd:d});' +
|
|
2320
|
+
'process.exit(r.status==null?1:r.status);'
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
export function buildRuvectorMcpAddCommand(env, cwd = RUVECTOR_MCP_CWD) {
|
|
2324
|
+
const launcher = buildRuvectorMcpLauncher(cwd);
|
|
2325
|
+
return {
|
|
2326
|
+
shell: env.claude ? ['claude', ['mcp', 'add', 'ruvector', '--scope', 'user', '--', 'node', '-e', launcher]] : null,
|
|
2327
|
+
say: `claude mcp add ruvector --scope user -- node -e '${launcher}'`,
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// โโ issue #39 mitigation: clean up the empty ruvector.db this bug already left behind in
|
|
2332
|
+
// projects that installed before this fix landed. Matches ONLY the exact empty-scaffold
|
|
2333
|
+
// signature (dimensions 384, Cosine, storage_path "./ruvector.db" โ literally the string the
|
|
2334
|
+
// buggy native default writes, verified live byte-for-byte) under a conservative size cap, so
|
|
2335
|
+
// a real store that just happens to share the filename is never at risk: a populated store
|
|
2336
|
+
// either exceeds the cap or lacks this exact header.
|
|
2337
|
+
const RUVECTOR_EMPTY_DB_SIGNATURE =
|
|
2338
|
+
'{"dimensions":384,"distance_metric":"Cosine","storage_path":"./ruvector.db","hnsw_config":null,"quantization":null}';
|
|
2339
|
+
const RUVECTOR_EMPTY_DB_MAX_BYTES = 3 * 1024 * 1024; // observed empty scaffold: 1,589,248 bytes; headroom, still far below any real dataset
|
|
2340
|
+
export function cleanupStrayRuvectorDb(dir = process.cwd()) {
|
|
2341
|
+
const p = path.join(dir, 'ruvector.db');
|
|
2342
|
+
let st;
|
|
2343
|
+
try {
|
|
2344
|
+
st = fs.statSync(p);
|
|
2345
|
+
} catch {
|
|
2346
|
+
return false; // doesn't exist โ nothing to do
|
|
2347
|
+
}
|
|
2348
|
+
if (!st.isFile() || st.size === 0 || st.size > RUVECTOR_EMPTY_DB_MAX_BYTES) return false;
|
|
2349
|
+
let buf;
|
|
2350
|
+
try {
|
|
2351
|
+
buf = fs.readFileSync(p);
|
|
2352
|
+
} catch {
|
|
2353
|
+
return false; // unreadable โ never guess, just leave it alone
|
|
2354
|
+
}
|
|
2355
|
+
if (!buf.includes(RUVECTOR_EMPTY_DB_SIGNATURE)) return false;
|
|
2356
|
+
try {
|
|
2357
|
+
fs.unlinkSync(p);
|
|
2358
|
+
} catch {
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
warn(`removed ${p} โ an empty container the ruvector MCP server left in this project before issue #39 was fixed (matched the known empty signature; nothing was stored in it)`);
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2119
2365
|
// โโ step: is the rUv toolkit here? the brain ANSWERS alone; it BUILDS best with Ruflo + RuVector โโ
|
|
2120
2366
|
async function offerStack(env) {
|
|
2121
2367
|
step(
|
|
2122
2368
|
'Checking your rUv toolkit',
|
|
2123
2369
|
"the brain answers on its own with zero setup โ but it can also BUILD, and that shines when the tools it recommends are here",
|
|
2124
2370
|
);
|
|
2371
|
+
cleanupStrayRuvectorDb(); // issue #39: sweep a leftover scaffold from before this fix, if one's here
|
|
2125
2372
|
const mark = (ok) => (ok ? c.green('โ present') : c.yellow('โ not found'));
|
|
2126
2373
|
info(`platform: ${c.bold(`${env.platform}/${env.arch}`)} ยท node ${c.bold(env.node)}`);
|
|
2127
2374
|
info(`Ruflo (claude-flow ยท swarms / SPARC / orchestration): ${mark(env.ruflo)}`);
|
|
@@ -2147,9 +2394,9 @@ async function offerStack(env) {
|
|
|
2147
2394
|
what: 'RuVector (vectors / RVF)',
|
|
2148
2395
|
// --scope user: same reason the plugin itself installs at user scope โ the brain is "one
|
|
2149
2396
|
// toolkit, every project", and the default "local" scope ties the server to whatever
|
|
2150
|
-
// directory happens to be the cwd when this runs.
|
|
2151
|
-
|
|
2152
|
-
|
|
2397
|
+
// directory happens to be the cwd when this runs. The registered command itself also
|
|
2398
|
+
// pins ITS OWN cwd (issue #39) โ see buildRuvectorMcpAddCommand above.
|
|
2399
|
+
...buildRuvectorMcpAddCommand(env),
|
|
2153
2400
|
plugin: null,
|
|
2154
2401
|
verify: () => hasUserScopeMcpServer('ruvector'),
|
|
2155
2402
|
});
|
|
@@ -2169,7 +2416,7 @@ async function offerStack(env) {
|
|
|
2169
2416
|
return;
|
|
2170
2417
|
}
|
|
2171
2418
|
|
|
2172
|
-
const yes = FLAG_WITH_STACK || (await ask('Add the missing rUv tools now so the brain can build, not just answer?', false));
|
|
2419
|
+
const yes = FLAG_WITH_STACK || (await ask('Add the missing rUv tools now so the brain can build, not just answer?', false, { planKey: 'stack' }));
|
|
2173
2420
|
if (!yes) {
|
|
2174
2421
|
info("No problem โ nothing's missing for answers. To add the build tools any time:");
|
|
2175
2422
|
printCmds();
|
|
@@ -2457,7 +2704,7 @@ export async function offerStatusline() {
|
|
|
2457
2704
|
return 'not-asked';
|
|
2458
2705
|
}
|
|
2459
2706
|
|
|
2460
|
-
const yes = FLAG_STATUSLINE || (await ask('Add a RuvNet Brain version segment to your Claude Code status bar?', false, { blanketYes: false }));
|
|
2707
|
+
const yes = FLAG_STATUSLINE || (await ask('Add a RuvNet Brain version segment to your Claude Code status bar?', false, { blanketYes: false, planKey: 'statusline' }));
|
|
2461
2708
|
|
|
2462
2709
|
try {
|
|
2463
2710
|
fs.mkdirSync(telemetryStateDir(), { recursive: true });
|
|
@@ -2768,6 +3015,9 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
2768
3015
|
|
|
2769
3016
|
installReader(cacheDir);
|
|
2770
3017
|
const plugin = wirePlugin();
|
|
3018
|
+
// Codex hosts got nothing before this (issue #42): shipped, never registered. Non-fatal like every
|
|
3019
|
+
// other wiring step โ a second host we cannot reach must never break the one we can.
|
|
3020
|
+
try { wireCodexHost(); } catch (e) { warn(`(Codex wiring skipped: ${e && e.message})`); }
|
|
2771
3021
|
if (!FLAG_NO_VERIFY) {
|
|
2772
3022
|
verifyInstall(cacheDir);
|
|
2773
3023
|
await smokeQuery(cacheDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.72-dev",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain \u2014 a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|