Skip to content

Commit b688273

Browse files
authored
feat: brain commands use direct pi.ruv.io REST API — no @ruvector/pi-brain dependency (#233)
Replace requirePiBrain() + PiBrainClient with direct fetch() calls to pi.ruv.io. All 13 brain CLI commands and 11 brain MCP tools now work out of the box with zero extra dependencies. Includes 30s timeout on all brain API calls.
1 parent c47d877 commit b688273

File tree

2 files changed

+109
-77
lines changed

2 files changed

+109
-77
lines changed

npm/packages/ruvector/bin/cli.js

Lines changed: 44 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7851,26 +7851,40 @@ mcpCmd.command('test')
78517851
});
78527852

78537853
// ============================================================================
7854-
// Brain Commands — Shared intelligence via @ruvector/pi-brain (lazy-loaded)
7854+
// Brain Commands — Shared intelligence via pi.ruv.io REST API (direct fetch)
78557855
// ============================================================================
78567856

7857-
async function requirePiBrain() {
7858-
try {
7859-
return require('@ruvector/pi-brain');
7860-
} catch {
7861-
console.error(chalk.red('Brain commands require @ruvector/pi-brain'));
7862-
console.error(chalk.yellow(' npm install @ruvector/pi-brain'));
7863-
process.exit(1);
7864-
}
7865-
}
7866-
78677857
function getBrainConfig(opts) {
78687858
return {
78697859
url: opts.url || process.env.BRAIN_URL || 'https://pi.ruv.io',
78707860
key: opts.key || process.env.PI
78717861
};
78727862
}
78737863

7864+
function brainHeaders(config) {
7865+
const h = { 'Content-Type': 'application/json' };
7866+
if (config.key) h['Authorization'] = `Bearer ${config.key}`;
7867+
return h;
7868+
}
7869+
7870+
async function brainFetch(config, endpoint, opts = {}) {
7871+
const url = new URL(endpoint, config.url);
7872+
if (opts.params) {
7873+
for (const [k, v] of Object.entries(opts.params)) {
7874+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
7875+
}
7876+
}
7877+
const fetchOpts = { headers: brainHeaders(config), signal: AbortSignal.timeout(30000) };
7878+
if (opts.method) fetchOpts.method = opts.method;
7879+
if (opts.body) { fetchOpts.method = opts.method || 'POST'; fetchOpts.body = JSON.stringify(opts.body); }
7880+
const resp = await fetch(url.toString(), fetchOpts);
7881+
if (!resp.ok) {
7882+
const errText = await resp.text().catch(() => resp.statusText);
7883+
throw new Error(`${resp.status} ${errText}`);
7884+
}
7885+
return resp.json();
7886+
}
7887+
78747888
const brainCmd = program.command('brain').description('Shared intelligence — search, share, and manage collective knowledge');
78757889

78767890
brainCmd.command('search <query>')
@@ -7882,11 +7896,9 @@ brainCmd.command('search <query>')
78827896
.option('--json', 'Output as JSON')
78837897
.option('--verbose', 'Show detailed scoring and metadata per result')
78847898
.action(async (query, opts) => {
7885-
const piBrain = await requirePiBrain();
78867899
const config = getBrainConfig(opts);
78877900
try {
7888-
const client = new piBrain.PiBrainClient(config);
7889-
const results = await client.search(query, { category: opts.category, limit: parseInt(opts.limit) });
7901+
const results = await brainFetch(config, '/v1/memories/search', { params: { q: query, category: opts.category, limit: opts.limit } });
78907902
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(results, null, 2)); return; }
78917903
console.log(chalk.bold.cyan(`\nBrain Search: "${query}"\n`));
78927904
if (!results.length) { console.log(chalk.dim(' No results found.\n')); return; }
@@ -7916,11 +7928,9 @@ brainCmd.command('share <title>')
79167928
.option('--url <url>', 'Brain server URL')
79177929
.option('--key <key>', 'Pi key')
79187930
.action(async (title, opts) => {
7919-
const piBrain = await requirePiBrain();
79207931
const config = getBrainConfig(opts);
79217932
try {
7922-
const client = new piBrain.PiBrainClient(config);
7923-
const result = await client.share({ title, content: opts.content || title, category: opts.category, tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : [], code_snippet: opts.code });
7933+
const result = await brainFetch(config, '/v1/memories', { body: { title, content: opts.content || title, category: opts.category, tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : [], code_snippet: opts.code } });
79247934
console.log(chalk.green(`Shared: ${result.id || 'OK'}`));
79257935
} catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
79267936
});
@@ -7931,11 +7941,9 @@ brainCmd.command('get <id>')
79317941
.option('--key <key>', 'Pi key')
79327942
.option('--json', 'Output as JSON')
79337943
.action(async (id, opts) => {
7934-
const piBrain = await requirePiBrain();
79357944
const config = getBrainConfig(opts);
79367945
try {
7937-
const client = new piBrain.PiBrainClient(config);
7938-
const result = await client.get(id);
7946+
const result = await brainFetch(config, `/v1/memories/${id}`);
79397947
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
79407948
console.log(chalk.bold.cyan(`\nMemory: ${id}\n`));
79417949
if (result.title) console.log(` ${chalk.bold('Title:')} ${result.title}`);
@@ -7950,11 +7958,9 @@ brainCmd.command('vote <id> <direction>')
79507958
.option('--url <url>', 'Brain server URL')
79517959
.option('--key <key>', 'Pi key')
79527960
.action(async (id, direction, opts) => {
7953-
const piBrain = await requirePiBrain();
79547961
const config = getBrainConfig(opts);
79557962
try {
7956-
const client = new piBrain.PiBrainClient(config);
7957-
await client.vote(id, direction);
7963+
await brainFetch(config, `/v1/memories/${id}/vote`, { body: { direction } });
79587964
console.log(chalk.green(`Voted ${direction} on ${id}`));
79597965
} catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
79607966
});
@@ -7967,11 +7973,9 @@ brainCmd.command('list')
79677973
.option('--key <key>', 'Pi key')
79687974
.option('--json', 'Output as JSON')
79697975
.action(async (opts) => {
7970-
const piBrain = await requirePiBrain();
79717976
const config = getBrainConfig(opts);
79727977
try {
7973-
const client = new piBrain.PiBrainClient(config);
7974-
const results = await client.list({ category: opts.category, limit: parseInt(opts.limit) });
7978+
const results = await brainFetch(config, '/v1/memories/list', { params: { category: opts.category, limit: opts.limit } });
79757979
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(results, null, 2)); return; }
79767980
console.log(chalk.bold.cyan('\nShared Brain Memories\n'));
79777981
if (!results.length) { console.log(chalk.dim(' No memories found.\n')); return; }
@@ -7987,11 +7991,9 @@ brainCmd.command('delete <id>')
79877991
.option('--url <url>', 'Brain server URL')
79887992
.option('--key <key>', 'Pi key')
79897993
.action(async (id, opts) => {
7990-
const piBrain = await requirePiBrain();
79917994
const config = getBrainConfig(opts);
79927995
try {
7993-
const client = new piBrain.PiBrainClient(config);
7994-
await client.delete(id);
7996+
await brainFetch(config, `/v1/memories/${id}`, { method: 'DELETE' });
79957997
console.log(chalk.green(`Deleted: ${id}`));
79967998
} catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
79977999
});
@@ -8002,11 +8004,9 @@ brainCmd.command('status')
80028004
.option('--key <key>', 'Pi key')
80038005
.option('--json', 'Output as JSON')
80048006
.action(async (opts) => {
8005-
const piBrain = await requirePiBrain();
80068007
const config = getBrainConfig(opts);
80078008
try {
8008-
const client = new piBrain.PiBrainClient(config);
8009-
const status = await client.status();
8009+
const status = await brainFetch(config, '/v1/status');
80108010
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(status, null, 2)); return; }
80118011
console.log(chalk.bold.cyan('\nBrain Status\n'));
80128012
Object.entries(status).forEach(([k, v]) => {
@@ -8037,11 +8037,9 @@ brainCmd.command('drift')
80378037
.option('--key <key>', 'Pi key')
80388038
.option('--json', 'Output as JSON')
80398039
.action(async (opts) => {
8040-
const piBrain = await requirePiBrain();
80418040
const config = getBrainConfig(opts);
80428041
try {
8043-
const client = new piBrain.PiBrainClient(config);
8044-
const report = await client.drift({ domain: opts.domain });
8042+
const report = await brainFetch(config, '/v1/drift', { params: { domain: opts.domain } });
80458043
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(report, null, 2)); return; }
80468044
console.log(chalk.bold.cyan('\nDrift Report\n'));
80478045
console.log(` ${chalk.bold('Drifting:')} ${report.is_drifting ? chalk.red('Yes') : chalk.green('No')}`);
@@ -8058,11 +8056,9 @@ brainCmd.command('partition')
80588056
.option('--key <key>', 'Pi key')
80598057
.option('--json', 'Output as JSON')
80608058
.action(async (opts) => {
8061-
const piBrain = await requirePiBrain();
80628059
const config = getBrainConfig(opts);
80638060
try {
8064-
const client = new piBrain.PiBrainClient(config);
8065-
const result = await client.partition({ domain: opts.domain, min_cluster_size: parseInt(opts.minSize) });
8061+
const result = await brainFetch(config, '/v1/partition', { params: { domain: opts.domain, min_cluster_size: opts.minSize } });
80668062
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
80678063
console.log(chalk.bold.cyan('\nKnowledge Partitions\n'));
80688064
if (result.clusters) {
@@ -8080,11 +8076,9 @@ brainCmd.command('transfer <source> <target>')
80808076
.option('--key <key>', 'Pi key')
80818077
.option('--json', 'Output as JSON')
80828078
.action(async (source, target, opts) => {
8083-
const piBrain = await requirePiBrain();
80848079
const config = getBrainConfig(opts);
80858080
try {
8086-
const client = new piBrain.PiBrainClient(config);
8087-
const result = await client.transfer(source, target);
8081+
const result = await brainFetch(config, '/v1/transfer', { body: { source_domain: source, target_domain: target } });
80888082
if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
80898083
console.log(chalk.green(`Transfer ${source} -> ${target}: ${result.status || 'OK'}`));
80908084
} catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
@@ -8095,11 +8089,9 @@ brainCmd.command('sync [direction]')
80958089
.option('--url <url>', 'Brain server URL')
80968090
.option('--key <key>', 'Pi key')
80978091
.action(async (direction, opts) => {
8098-
const piBrain = await requirePiBrain();
80998092
const config = getBrainConfig(opts);
81008093
try {
8101-
const client = new piBrain.PiBrainClient(config);
8102-
const result = await client.sync(direction || 'both');
8094+
const result = await brainFetch(config, '/v1/lora/latest', { params: { direction: direction || 'both' } });
81038095
console.log(chalk.green(`Sync ${direction || 'both'}: ${result.status || 'OK'}`));
81048096
} catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
81058097
});
@@ -8110,30 +8102,28 @@ brainCmd.command('page <action> [args...]')
81108102
.option('--key <key>', 'Pi key')
81118103
.option('--json', 'Output as JSON')
81128104
.action(async (action, args, opts) => {
8113-
const piBrain = await requirePiBrain();
81148105
const config = getBrainConfig(opts);
81158106
try {
8116-
const client = new piBrain.PiBrainClient(config);
81178107
let result;
81188108
switch (action) {
81198109
case 'list':
8120-
result = await client.listPages ? client.listPages({ limit: 20 }) : { pages: [], message: 'Brainpedia not yet available on this server' };
8110+
result = await brainFetch(config, '/v1/pages', { params: { limit: 20 } }).catch(() => ({ pages: [], message: 'Brainpedia endpoint not available' }));
81218111
break;
81228112
case 'get':
81238113
if (!args[0]) { console.error(chalk.red('Usage: brain page get <slug>')); process.exit(1); }
8124-
result = await client.getPage ? client.getPage(args[0]) : { error: 'Brainpedia not yet available' };
8114+
result = await brainFetch(config, `/v1/pages/${args[0]}`);
81258115
break;
81268116
case 'create':
81278117
if (!args[0]) { console.error(chalk.red('Usage: brain page create <title> [--content <text>]')); process.exit(1); }
8128-
result = await client.createPage ? client.createPage({ title: args[0], content: opts.content || '' }) : { error: 'Brainpedia not yet available' };
8118+
result = await brainFetch(config, '/v1/pages', { body: { title: args[0], content: opts.content || '' } });
81298119
break;
81308120
case 'update':
81318121
if (!args[0]) { console.error(chalk.red('Usage: brain page update <slug> [--content <text>]')); process.exit(1); }
8132-
result = await client.updatePage ? client.updatePage(args[0], { content: opts.content || '' }) : { error: 'Brainpedia not yet available' };
8122+
result = await brainFetch(config, `/v1/pages/${args[0]}/deltas`, { body: { content: opts.content || '' } });
81338123
break;
81348124
case 'delete':
81358125
if (!args[0]) { console.error(chalk.red('Usage: brain page delete <slug>')); process.exit(1); }
8136-
result = await client.deletePage ? client.deletePage(args[0]) : { error: 'Brainpedia not yet available' };
8126+
result = await brainFetch(config, `/v1/pages/${args[0]}`, { method: 'DELETE' }).catch(() => ({ error: 'Delete not available' }));
81378127
break;
81388128
default:
81398129
console.error(chalk.red(`Unknown page action: ${action}. Use: list, get, create, update, delete`));
@@ -8159,25 +8149,23 @@ brainCmd.command('node <action> [args...]')
81598149
.option('--key <key>', 'Pi key')
81608150
.option('--json', 'Output as JSON')
81618151
.action(async (action, args, opts) => {
8162-
const piBrain = await requirePiBrain();
81638152
const config = getBrainConfig(opts);
81648153
try {
8165-
const client = new piBrain.PiBrainClient(config);
81668154
let result;
81678155
switch (action) {
81688156
case 'publish':
81698157
if (!args[0]) { console.error(chalk.red('Usage: brain node publish <wasm-file>')); process.exit(1); }
81708158
const wasmPath = path.resolve(args[0]);
81718159
if (!fs.existsSync(wasmPath)) { console.error(chalk.red(`File not found: ${wasmPath}`)); process.exit(1); }
81728160
const wasmBytes = fs.readFileSync(wasmPath);
8173-
result = await client.publishNode ? client.publishNode({ wasm: wasmBytes, name: path.basename(wasmPath, '.wasm') }) : { error: 'WASM node publish not yet available on this server' };
8161+
result = await brainFetch(config, '/v1/nodes', { body: { name: path.basename(wasmPath, '.wasm'), wasm_base64: wasmBytes.toString('base64') } });
81748162
break;
81758163
case 'list':
8176-
result = await client.listNodes ? client.listNodes({ limit: 20 }) : { nodes: [], message: 'WASM node listing not yet available' };
8164+
result = await brainFetch(config, '/v1/nodes', { params: { limit: 20 } }).catch(() => ({ nodes: [], message: 'WASM node listing not available' }));
81778165
break;
81788166
case 'status':
81798167
if (!args[0]) { console.error(chalk.red('Usage: brain node status <node-id>')); process.exit(1); }
8180-
result = await client.nodeStatus ? client.nodeStatus(args[0]) : { error: 'WASM node status not yet available' };
8168+
result = await brainFetch(config, `/v1/nodes/${args[0]}`);
81818169
break;
81828170
default:
81838171
console.error(chalk.red(`Unknown node action: ${action}. Use: publish, list, status`));

npm/packages/ruvector/bin/mcp-server.js

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3276,7 +3276,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
32763276
}
32773277
}
32783278

3279-
// ── Brain Tool Handlers ──────────────────────────────────────────────
3279+
// ── Brain Tool Handlers (direct fetch to pi.ruv.io) ─────────────────
32803280
case 'brain_search':
32813281
case 'brain_share':
32823282
case 'brain_get':
@@ -3289,31 +3289,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
32893289
case 'brain_transfer':
32903290
case 'brain_sync': {
32913291
try {
3292-
const { PiBrainClient } = require('@ruvector/pi-brain');
3293-
const client = new PiBrainClient({
3294-
url: process.env.BRAIN_URL || 'https://pi.ruv.io',
3295-
key: process.env.PI
3296-
});
3292+
const brainUrl = process.env.BRAIN_URL || 'https://pi.ruv.io';
3293+
const brainKey = process.env.PI;
3294+
const hdrs = { 'Content-Type': 'application/json' };
3295+
if (brainKey) hdrs['Authorization'] = `Bearer ${brainKey}`;
32973296
const subCmd = name.replace('brain_', '');
3298-
let result;
3297+
let url, fetchOpts = { headers: hdrs, signal: AbortSignal.timeout(30000) };
32993298
switch (subCmd) {
3300-
case 'search': result = await client.search(args.query, { category: args.category, limit: args.limit || 10 }); break;
3301-
case 'share': result = await client.share({ title: args.title, content: args.content, category: args.category, tags: args.tags ? args.tags.split(',').map(t => t.trim()) : [], code_snippet: args.code_snippet }); break;
3302-
case 'get': result = await client.get(args.id); break;
3303-
case 'vote': result = await client.vote(args.id, args.direction); break;
3304-
case 'list': result = await client.list({ category: args.category, limit: args.limit || 20 }); break;
3305-
case 'delete': result = await client.delete(args.id); break;
3306-
case 'status': result = await client.status(); break;
3307-
case 'drift': result = await client.drift({ domain: args.domain }); break;
3308-
case 'partition': result = await client.partition({ domain: args.domain, min_cluster_size: args.min_cluster_size }); break;
3309-
case 'transfer': result = await client.transfer(args.source_domain, args.target_domain); break;
3310-
case 'sync': result = await client.sync(args.direction || 'both'); break;
3299+
case 'search': {
3300+
const p = new URLSearchParams({ q: args.query || '' });
3301+
if (args.category) p.set('category', args.category);
3302+
if (args.limit) p.set('limit', String(args.limit));
3303+
url = `${brainUrl}/v1/memories/search?${p}`;
3304+
break;
3305+
}
3306+
case 'share': {
3307+
url = `${brainUrl}/v1/memories`;
3308+
fetchOpts.method = 'POST';
3309+
fetchOpts.body = JSON.stringify({ title: args.title, content: args.content, category: args.category, tags: args.tags ? args.tags.split(',').map(t => t.trim()) : [], code_snippet: args.code_snippet });
3310+
break;
3311+
}
3312+
case 'get': url = `${brainUrl}/v1/memories/${args.id}`; break;
3313+
case 'vote': {
3314+
url = `${brainUrl}/v1/memories/${args.id}/vote`;
3315+
fetchOpts.method = 'POST';
3316+
fetchOpts.body = JSON.stringify({ direction: args.direction });
3317+
break;
3318+
}
3319+
case 'list': {
3320+
const p = new URLSearchParams();
3321+
if (args.category) p.set('category', args.category);
3322+
p.set('limit', String(args.limit || 20));
3323+
url = `${brainUrl}/v1/memories/list?${p}`;
3324+
break;
3325+
}
3326+
case 'delete': {
3327+
url = `${brainUrl}/v1/memories/${args.id}`;
3328+
fetchOpts.method = 'DELETE';
3329+
break;
3330+
}
3331+
case 'status': url = `${brainUrl}/v1/status`; break;
3332+
case 'drift': {
3333+
const p = new URLSearchParams();
3334+
if (args.domain) p.set('domain', args.domain);
3335+
url = `${brainUrl}/v1/drift?${p}`;
3336+
break;
3337+
}
3338+
case 'partition': {
3339+
const p = new URLSearchParams();
3340+
if (args.domain) p.set('domain', args.domain);
3341+
if (args.min_cluster_size) p.set('min_cluster_size', String(args.min_cluster_size));
3342+
url = `${brainUrl}/v1/partition?${p}`;
3343+
break;
3344+
}
3345+
case 'transfer': {
3346+
url = `${brainUrl}/v1/transfer`;
3347+
fetchOpts.method = 'POST';
3348+
fetchOpts.body = JSON.stringify({ source_domain: args.source_domain, target_domain: args.target_domain });
3349+
break;
3350+
}
3351+
case 'sync': url = `${brainUrl}/v1/lora/latest`; break;
33113352
}
3353+
const resp = await fetch(url, fetchOpts);
3354+
if (!resp.ok) {
3355+
const errText = await resp.text().catch(() => resp.statusText);
3356+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `${resp.status} ${errText}` }, null, 2) }], isError: true };
3357+
}
3358+
const result = await resp.json();
33123359
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...result }, null, 2) }] };
33133360
} catch (e) {
3314-
if (e.code === 'MODULE_NOT_FOUND') {
3315-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Brain tools require @ruvector/pi-brain. Install with: npm install @ruvector/pi-brain' }, null, 2) }], isError: true };
3316-
}
33173361
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: e.message }, null, 2) }], isError: true };
33183362
}
33193363
}

0 commit comments

Comments
 (0)