ruvector 0.2.5 → 0.2.7

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.
Files changed (3) hide show
  1. package/bin/cli.js +133 -67
  2. package/bin/mcp-server.js +143 -31
  3. package/package.json +1 -3
package/bin/cli.js CHANGED
@@ -115,6 +115,76 @@ function loadGnn() {
115
115
  }
116
116
  }
117
117
 
118
+ // ── Proxy-aware fetch wrapper ───────────────────────────────────────────────
119
+ // Detects HTTP_PROXY / HTTPS_PROXY / ALL_PROXY env vars and routes traffic
120
+ // through the proxy when configured. Falls back to native fetch() when no
121
+ // proxy is set or the target matches NO_PROXY.
122
+ let _proxyDispatcherSet = false;
123
+
124
+ function getProxyUrl(targetUrl) {
125
+ const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || '';
126
+ if (NO_PROXY === '*') return null;
127
+ if (NO_PROXY) {
128
+ try {
129
+ const host = new URL(targetUrl).hostname.toLowerCase();
130
+ const patterns = NO_PROXY.split(',').map(p => p.trim().toLowerCase());
131
+ for (const p of patterns) {
132
+ if (!p) continue;
133
+ if (host === p || host.endsWith(p.startsWith('.') ? p : '.' + p)) return null;
134
+ }
135
+ } catch {}
136
+ }
137
+ return process.env.HTTPS_PROXY || process.env.https_proxy
138
+ || process.env.HTTP_PROXY || process.env.http_proxy
139
+ || process.env.ALL_PROXY || process.env.all_proxy
140
+ || null;
141
+ }
142
+
143
+ async function proxyFetch(url, opts) {
144
+ const target = typeof url === 'string' ? url : url.toString();
145
+ const proxy = getProxyUrl(target);
146
+ if (!proxy) return fetch(url, opts);
147
+
148
+ // Try undici ProxyAgent (bundled in Node 18+)
149
+ if (!_proxyDispatcherSet) {
150
+ try {
151
+ const undici = require('undici');
152
+ if (undici.ProxyAgent && undici.setGlobalDispatcher) {
153
+ undici.setGlobalDispatcher(new undici.ProxyAgent(proxy));
154
+ _proxyDispatcherSet = true;
155
+ }
156
+ } catch {}
157
+ }
158
+ if (_proxyDispatcherSet) return fetch(url, opts);
159
+
160
+ // Fallback: shell out to curl (which natively respects proxy env vars)
161
+ const { execFileSync } = require('child_process');
162
+ const args = ['-sS', '-L', '--max-time', '30'];
163
+ if (opts && opts.method) { args.push('-X', opts.method); }
164
+ if (opts && opts.headers) {
165
+ for (const [k, v] of Object.entries(opts.headers)) {
166
+ args.push('-H', `${k}: ${v}`);
167
+ }
168
+ }
169
+ if (opts && opts.body) { args.push('-d', typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body)); }
170
+ args.push(target);
171
+ try {
172
+ const stdout = execFileSync('curl', args, { encoding: 'utf8', timeout: 35000 });
173
+ const body = stdout.trim();
174
+ return {
175
+ ok: true,
176
+ status: 200,
177
+ statusText: 'OK',
178
+ text: async () => body,
179
+ json: async () => JSON.parse(body),
180
+ headers: new Map(),
181
+ };
182
+ } catch (e) {
183
+ const msg = e.stderr ? e.stderr.toString().trim() : e.message;
184
+ throw new Error(`Proxy curl failed: ${msg}`);
185
+ }
186
+ }
187
+
118
188
  // Lazy load Attention (optional - loaded on first use, not at startup)
119
189
  let _attentionModule = undefined;
120
190
  let DotProductAttention, MultiHeadAttention, HyperbolicAttention, FlashAttention, LinearAttention, MoEAttention;
@@ -7250,7 +7320,7 @@ async function getRvfManifest(opts = {}) {
7250
7320
 
7251
7321
  // Try GCS
7252
7322
  try {
7253
- const resp = await fetch(GCS_MANIFEST_URL);
7323
+ const resp = await proxyFetch(GCS_MANIFEST_URL, { signal: AbortSignal.timeout(15000) });
7254
7324
  if (resp.ok) {
7255
7325
  const manifest = await resp.json();
7256
7326
  fs.mkdirSync(cacheDir, { recursive: true });
@@ -7851,19 +7921,9 @@ mcpCmd.command('test')
7851
7921
  });
7852
7922
 
7853
7923
  // ============================================================================
7854
- // Brain Commands — Shared intelligence via @ruvector/pi-brain (lazy-loaded)
7924
+ // Brain Commands — Shared intelligence via pi.ruv.io REST API (direct fetch)
7855
7925
  // ============================================================================
7856
7926
 
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
-
7867
7927
  function getBrainConfig(opts) {
7868
7928
  return {
7869
7929
  url: opts.url || process.env.BRAIN_URL || 'https://pi.ruv.io',
@@ -7871,6 +7931,30 @@ function getBrainConfig(opts) {
7871
7931
  };
7872
7932
  }
7873
7933
 
7934
+ function brainHeaders(config) {
7935
+ const h = { 'Content-Type': 'application/json' };
7936
+ if (config.key) h['Authorization'] = `Bearer ${config.key}`;
7937
+ return h;
7938
+ }
7939
+
7940
+ async function brainFetch(config, endpoint, opts = {}) {
7941
+ const url = new URL(endpoint, config.url);
7942
+ if (opts.params) {
7943
+ for (const [k, v] of Object.entries(opts.params)) {
7944
+ if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
7945
+ }
7946
+ }
7947
+ const fetchOpts = { headers: brainHeaders(config), signal: AbortSignal.timeout(30000) };
7948
+ if (opts.method) fetchOpts.method = opts.method;
7949
+ if (opts.body) { fetchOpts.method = opts.method || 'POST'; fetchOpts.body = JSON.stringify(opts.body); }
7950
+ const resp = await proxyFetch(url.toString(), fetchOpts);
7951
+ if (!resp.ok) {
7952
+ const errText = await resp.text().catch(() => resp.statusText);
7953
+ throw new Error(`${resp.status} ${errText}`);
7954
+ }
7955
+ return resp.json();
7956
+ }
7957
+
7874
7958
  const brainCmd = program.command('brain').description('Shared intelligence — search, share, and manage collective knowledge');
7875
7959
 
7876
7960
  brainCmd.command('search <query>')
@@ -7882,11 +7966,9 @@ brainCmd.command('search <query>')
7882
7966
  .option('--json', 'Output as JSON')
7883
7967
  .option('--verbose', 'Show detailed scoring and metadata per result')
7884
7968
  .action(async (query, opts) => {
7885
- const piBrain = await requirePiBrain();
7886
7969
  const config = getBrainConfig(opts);
7887
7970
  try {
7888
- const client = new piBrain.PiBrainClient(config);
7889
- const results = await client.search(query, { category: opts.category, limit: parseInt(opts.limit) });
7971
+ const results = await brainFetch(config, '/v1/memories/search', { params: { q: query, category: opts.category, limit: opts.limit } });
7890
7972
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(results, null, 2)); return; }
7891
7973
  console.log(chalk.bold.cyan(`\nBrain Search: "${query}"\n`));
7892
7974
  if (!results.length) { console.log(chalk.dim(' No results found.\n')); return; }
@@ -7916,11 +7998,9 @@ brainCmd.command('share <title>')
7916
7998
  .option('--url <url>', 'Brain server URL')
7917
7999
  .option('--key <key>', 'Pi key')
7918
8000
  .action(async (title, opts) => {
7919
- const piBrain = await requirePiBrain();
7920
8001
  const config = getBrainConfig(opts);
7921
8002
  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 });
8003
+ 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 } });
7924
8004
  console.log(chalk.green(`Shared: ${result.id || 'OK'}`));
7925
8005
  } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7926
8006
  });
@@ -7931,11 +8011,9 @@ brainCmd.command('get <id>')
7931
8011
  .option('--key <key>', 'Pi key')
7932
8012
  .option('--json', 'Output as JSON')
7933
8013
  .action(async (id, opts) => {
7934
- const piBrain = await requirePiBrain();
7935
8014
  const config = getBrainConfig(opts);
7936
8015
  try {
7937
- const client = new piBrain.PiBrainClient(config);
7938
- const result = await client.get(id);
8016
+ const result = await brainFetch(config, `/v1/memories/${id}`);
7939
8017
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
7940
8018
  console.log(chalk.bold.cyan(`\nMemory: ${id}\n`));
7941
8019
  if (result.title) console.log(` ${chalk.bold('Title:')} ${result.title}`);
@@ -7950,11 +8028,9 @@ brainCmd.command('vote <id> <direction>')
7950
8028
  .option('--url <url>', 'Brain server URL')
7951
8029
  .option('--key <key>', 'Pi key')
7952
8030
  .action(async (id, direction, opts) => {
7953
- const piBrain = await requirePiBrain();
7954
8031
  const config = getBrainConfig(opts);
7955
8032
  try {
7956
- const client = new piBrain.PiBrainClient(config);
7957
- await client.vote(id, direction);
8033
+ await brainFetch(config, `/v1/memories/${id}/vote`, { body: { direction } });
7958
8034
  console.log(chalk.green(`Voted ${direction} on ${id}`));
7959
8035
  } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7960
8036
  });
@@ -7966,16 +8042,21 @@ brainCmd.command('list')
7966
8042
  .option('--url <url>', 'Brain server URL')
7967
8043
  .option('--key <key>', 'Pi key')
7968
8044
  .option('--json', 'Output as JSON')
8045
+ .option('--offset <n>', 'Skip first N results (pagination)', '0')
8046
+ .option('--sort <field>', 'Sort by: updated_at, quality, votes', 'updated_at')
8047
+ .option('--tags <tags>', 'Filter by tags (comma-separated)')
7969
8048
  .action(async (opts) => {
7970
- const piBrain = await requirePiBrain();
7971
8049
  const config = getBrainConfig(opts);
7972
8050
  try {
7973
- const client = new piBrain.PiBrainClient(config);
7974
- const results = await client.list({ category: opts.category, limit: parseInt(opts.limit) });
7975
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(results, null, 2)); return; }
8051
+ const data = await brainFetch(config, '/v1/memories/list', { params: { category: opts.category, limit: opts.limit, offset: opts.offset, sort: opts.sort, tags: opts.tags } });
8052
+ if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
7976
8053
  console.log(chalk.bold.cyan('\nShared Brain Memories\n'));
7977
- if (!results.length) { console.log(chalk.dim(' No memories found.\n')); return; }
7978
- results.forEach((r, i) => {
8054
+ const memories = Array.isArray(data) ? data : data.memories || [];
8055
+ if (data.total_count !== undefined) {
8056
+ console.log(chalk.dim(` Showing ${memories.length} of ${data.total_count} (offset ${data.offset || 0})\n`));
8057
+ }
8058
+ if (!memories.length) { console.log(chalk.dim(' No memories found.\n')); return; }
8059
+ memories.forEach((r, i) => {
7979
8060
  console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(r.title || r.id)} ${chalk.dim(`[${r.category || 'unknown'}]`)}`);
7980
8061
  });
7981
8062
  console.log();
@@ -7987,11 +8068,9 @@ brainCmd.command('delete <id>')
7987
8068
  .option('--url <url>', 'Brain server URL')
7988
8069
  .option('--key <key>', 'Pi key')
7989
8070
  .action(async (id, opts) => {
7990
- const piBrain = await requirePiBrain();
7991
8071
  const config = getBrainConfig(opts);
7992
8072
  try {
7993
- const client = new piBrain.PiBrainClient(config);
7994
- await client.delete(id);
8073
+ await brainFetch(config, `/v1/memories/${id}`, { method: 'DELETE' });
7995
8074
  console.log(chalk.green(`Deleted: ${id}`));
7996
8075
  } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7997
8076
  });
@@ -8002,11 +8081,9 @@ brainCmd.command('status')
8002
8081
  .option('--key <key>', 'Pi key')
8003
8082
  .option('--json', 'Output as JSON')
8004
8083
  .action(async (opts) => {
8005
- const piBrain = await requirePiBrain();
8006
8084
  const config = getBrainConfig(opts);
8007
8085
  try {
8008
- const client = new piBrain.PiBrainClient(config);
8009
- const status = await client.status();
8086
+ const status = await brainFetch(config, '/v1/status');
8010
8087
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(status, null, 2)); return; }
8011
8088
  console.log(chalk.bold.cyan('\nBrain Status\n'));
8012
8089
  Object.entries(status).forEach(([k, v]) => {
@@ -8037,11 +8114,9 @@ brainCmd.command('drift')
8037
8114
  .option('--key <key>', 'Pi key')
8038
8115
  .option('--json', 'Output as JSON')
8039
8116
  .action(async (opts) => {
8040
- const piBrain = await requirePiBrain();
8041
8117
  const config = getBrainConfig(opts);
8042
8118
  try {
8043
- const client = new piBrain.PiBrainClient(config);
8044
- const report = await client.drift({ domain: opts.domain });
8119
+ const report = await brainFetch(config, '/v1/drift', { params: { domain: opts.domain } });
8045
8120
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(report, null, 2)); return; }
8046
8121
  console.log(chalk.bold.cyan('\nDrift Report\n'));
8047
8122
  console.log(` ${chalk.bold('Drifting:')} ${report.is_drifting ? chalk.red('Yes') : chalk.green('No')}`);
@@ -8058,11 +8133,9 @@ brainCmd.command('partition')
8058
8133
  .option('--key <key>', 'Pi key')
8059
8134
  .option('--json', 'Output as JSON')
8060
8135
  .action(async (opts) => {
8061
- const piBrain = await requirePiBrain();
8062
8136
  const config = getBrainConfig(opts);
8063
8137
  try {
8064
- const client = new piBrain.PiBrainClient(config);
8065
- const result = await client.partition({ domain: opts.domain, min_cluster_size: parseInt(opts.minSize) });
8138
+ const result = await brainFetch(config, '/v1/partition', { params: { domain: opts.domain, min_cluster_size: opts.minSize } });
8066
8139
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8067
8140
  console.log(chalk.bold.cyan('\nKnowledge Partitions\n'));
8068
8141
  if (result.clusters) {
@@ -8080,11 +8153,9 @@ brainCmd.command('transfer <source> <target>')
8080
8153
  .option('--key <key>', 'Pi key')
8081
8154
  .option('--json', 'Output as JSON')
8082
8155
  .action(async (source, target, opts) => {
8083
- const piBrain = await requirePiBrain();
8084
8156
  const config = getBrainConfig(opts);
8085
8157
  try {
8086
- const client = new piBrain.PiBrainClient(config);
8087
- const result = await client.transfer(source, target);
8158
+ const result = await brainFetch(config, '/v1/transfer', { body: { source_domain: source, target_domain: target } });
8088
8159
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8089
8160
  console.log(chalk.green(`Transfer ${source} -> ${target}: ${result.status || 'OK'}`));
8090
8161
  } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
@@ -8095,11 +8166,9 @@ brainCmd.command('sync [direction]')
8095
8166
  .option('--url <url>', 'Brain server URL')
8096
8167
  .option('--key <key>', 'Pi key')
8097
8168
  .action(async (direction, opts) => {
8098
- const piBrain = await requirePiBrain();
8099
8169
  const config = getBrainConfig(opts);
8100
8170
  try {
8101
- const client = new piBrain.PiBrainClient(config);
8102
- const result = await client.sync(direction || 'both');
8171
+ const result = await brainFetch(config, '/v1/lora/latest', { params: { direction: direction || 'both' } });
8103
8172
  console.log(chalk.green(`Sync ${direction || 'both'}: ${result.status || 'OK'}`));
8104
8173
  } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8105
8174
  });
@@ -8110,30 +8179,28 @@ brainCmd.command('page <action> [args...]')
8110
8179
  .option('--key <key>', 'Pi key')
8111
8180
  .option('--json', 'Output as JSON')
8112
8181
  .action(async (action, args, opts) => {
8113
- const piBrain = await requirePiBrain();
8114
8182
  const config = getBrainConfig(opts);
8115
8183
  try {
8116
- const client = new piBrain.PiBrainClient(config);
8117
8184
  let result;
8118
8185
  switch (action) {
8119
8186
  case 'list':
8120
- result = await client.listPages ? client.listPages({ limit: 20 }) : { pages: [], message: 'Brainpedia not yet available on this server' };
8187
+ result = await brainFetch(config, '/v1/pages', { params: { limit: 20 } }).catch(() => ({ pages: [], message: 'Brainpedia endpoint not available' }));
8121
8188
  break;
8122
8189
  case 'get':
8123
8190
  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' };
8191
+ result = await brainFetch(config, `/v1/pages/${args[0]}`);
8125
8192
  break;
8126
8193
  case 'create':
8127
8194
  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' };
8195
+ result = await brainFetch(config, '/v1/pages', { body: { title: args[0], content: opts.content || '' } });
8129
8196
  break;
8130
8197
  case 'update':
8131
8198
  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' };
8199
+ result = await brainFetch(config, `/v1/pages/${args[0]}/deltas`, { body: { content: opts.content || '' } });
8133
8200
  break;
8134
8201
  case 'delete':
8135
8202
  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' };
8203
+ result = await brainFetch(config, `/v1/pages/${args[0]}`, { method: 'DELETE' }).catch(() => ({ error: 'Delete not available' }));
8137
8204
  break;
8138
8205
  default:
8139
8206
  console.error(chalk.red(`Unknown page action: ${action}. Use: list, get, create, update, delete`));
@@ -8159,10 +8226,8 @@ brainCmd.command('node <action> [args...]')
8159
8226
  .option('--key <key>', 'Pi key')
8160
8227
  .option('--json', 'Output as JSON')
8161
8228
  .action(async (action, args, opts) => {
8162
- const piBrain = await requirePiBrain();
8163
8229
  const config = getBrainConfig(opts);
8164
8230
  try {
8165
- const client = new piBrain.PiBrainClient(config);
8166
8231
  let result;
8167
8232
  switch (action) {
8168
8233
  case 'publish':
@@ -8170,14 +8235,14 @@ brainCmd.command('node <action> [args...]')
8170
8235
  const wasmPath = path.resolve(args[0]);
8171
8236
  if (!fs.existsSync(wasmPath)) { console.error(chalk.red(`File not found: ${wasmPath}`)); process.exit(1); }
8172
8237
  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' };
8238
+ result = await brainFetch(config, '/v1/nodes', { body: { name: path.basename(wasmPath, '.wasm'), wasm_base64: wasmBytes.toString('base64') } });
8174
8239
  break;
8175
8240
  case 'list':
8176
- result = await client.listNodes ? client.listNodes({ limit: 20 }) : { nodes: [], message: 'WASM node listing not yet available' };
8241
+ result = await brainFetch(config, '/v1/nodes', { params: { limit: 20 } }).catch(() => ({ nodes: [], message: 'WASM node listing not available' }));
8177
8242
  break;
8178
8243
  case 'status':
8179
8244
  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' };
8245
+ result = await brainFetch(config, `/v1/nodes/${args[0]}`);
8181
8246
  break;
8182
8247
  default:
8183
8248
  console.error(chalk.red(`Unknown node action: ${action}. Use: publish, list, status`));
@@ -8203,7 +8268,7 @@ async function fetchBrainEndpoint(config, endpoint) {
8203
8268
  const url = (config.url || 'https://pi.ruv.io') + endpoint;
8204
8269
  const headers = {};
8205
8270
  if (config.key) headers['Authorization'] = `Bearer ${config.key}`;
8206
- const resp = await fetch(url, { headers });
8271
+ const resp = await proxyFetch(url, { headers, signal: AbortSignal.timeout(30000) });
8207
8272
  if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
8208
8273
  return resp.json();
8209
8274
  }
@@ -8432,7 +8497,7 @@ midstreamCmd.command('benchmark')
8432
8497
 
8433
8498
  async function timeRequest(url, label) {
8434
8499
  const start = performance.now();
8435
- const resp = await fetch(url, { headers });
8500
+ const resp = await proxyFetch(url, { headers, signal: AbortSignal.timeout(30000) });
8436
8501
  const elapsed = performance.now() - start;
8437
8502
  return { label, status: resp.status, elapsed };
8438
8503
  }
@@ -8502,7 +8567,7 @@ edgeCmd.command('status')
8502
8567
  .option('--json', 'Output as JSON')
8503
8568
  .action(async (opts) => {
8504
8569
  try {
8505
- const resp = await fetch(`${EDGE_GENESIS}/status`);
8570
+ const resp = await proxyFetch(`${EDGE_GENESIS}/status`, { signal: AbortSignal.timeout(30000) });
8506
8571
  const data = await resp.json();
8507
8572
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8508
8573
  console.log(chalk.bold.cyan('\nEdge Network Status\n'));
@@ -8518,10 +8583,11 @@ edgeCmd.command('join')
8518
8583
  const piKey = process.env.PI;
8519
8584
  if (!piKey) { console.error(chalk.red('Set PI environment variable first. Run: npx ruvector identity generate')); process.exit(1); }
8520
8585
  try {
8521
- const resp = await fetch(`${EDGE_GENESIS}/join`, {
8586
+ const resp = await proxyFetch(`${EDGE_GENESIS}/join`, {
8522
8587
  method: 'POST',
8523
8588
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${piKey}` },
8524
- body: JSON.stringify({ contribution: parseFloat(opts.contribution), pi_key: piKey })
8589
+ body: JSON.stringify({ contribution: parseFloat(opts.contribution), pi_key: piKey }),
8590
+ signal: AbortSignal.timeout(30000)
8525
8591
  });
8526
8592
  const data = await resp.json();
8527
8593
  console.log(chalk.green(`Joined edge network: ${data.node_id || 'OK'}`));
@@ -8536,7 +8602,7 @@ edgeCmd.command('balance')
8536
8602
  if (!piKey) { console.error(chalk.red('Set PI environment variable first.')); process.exit(1); }
8537
8603
  try {
8538
8604
  const pseudonym = require('crypto').createHash('shake256', { outputLength: 16 }).update(piKey).digest('hex');
8539
- const resp = await fetch(`${EDGE_GENESIS}/balance/${pseudonym}`, { headers: { 'Authorization': `Bearer ${piKey}` } });
8605
+ const resp = await proxyFetch(`${EDGE_GENESIS}/balance/${pseudonym}`, { headers: { 'Authorization': `Bearer ${piKey}` }, signal: AbortSignal.timeout(30000) });
8540
8606
  if (!resp.ok) { console.error(chalk.red(`Edge network returned ${resp.status} ${resp.statusText}`)); process.exit(1); }
8541
8607
  const data = await resp.json();
8542
8608
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
@@ -8552,7 +8618,7 @@ edgeCmd.command('tasks')
8552
8618
  .option('--json', 'Output as JSON')
8553
8619
  .action(async (opts) => {
8554
8620
  try {
8555
- const resp = await fetch(`${EDGE_GENESIS}/tasks?limit=${opts.limit}`);
8621
+ const resp = await proxyFetch(`${EDGE_GENESIS}/tasks?limit=${opts.limit}`, { signal: AbortSignal.timeout(30000) });
8556
8622
  const data = await resp.json();
8557
8623
  if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8558
8624
  console.log(chalk.bold.cyan('\nEdge Compute Tasks\n'));
package/bin/mcp-server.js CHANGED
@@ -95,6 +95,71 @@ function sanitizeNumericArg(arg, defaultVal) {
95
95
  return Number.isFinite(n) && n > 0 ? n : (defaultVal || 0);
96
96
  }
97
97
 
98
+ // ── Proxy-aware fetch wrapper ───────────────────────────────────────────────
99
+ let _proxyDispatcherSet = false;
100
+
101
+ function getProxyUrl(targetUrl) {
102
+ const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || '';
103
+ if (NO_PROXY === '*') return null;
104
+ if (NO_PROXY) {
105
+ try {
106
+ const host = new URL(targetUrl).hostname.toLowerCase();
107
+ const patterns = NO_PROXY.split(',').map(p => p.trim().toLowerCase());
108
+ for (const p of patterns) {
109
+ if (!p) continue;
110
+ if (host === p || host.endsWith(p.startsWith('.') ? p : '.' + p)) return null;
111
+ }
112
+ } catch {}
113
+ }
114
+ return process.env.HTTPS_PROXY || process.env.https_proxy
115
+ || process.env.HTTP_PROXY || process.env.http_proxy
116
+ || process.env.ALL_PROXY || process.env.all_proxy
117
+ || null;
118
+ }
119
+
120
+ async function proxyFetch(url, opts) {
121
+ const target = typeof url === 'string' ? url : url.toString();
122
+ const proxy = getProxyUrl(target);
123
+ if (!proxy) return fetch(url, opts);
124
+
125
+ if (!_proxyDispatcherSet) {
126
+ try {
127
+ const undici = require('undici');
128
+ if (undici.ProxyAgent && undici.setGlobalDispatcher) {
129
+ undici.setGlobalDispatcher(new undici.ProxyAgent(proxy));
130
+ _proxyDispatcherSet = true;
131
+ }
132
+ } catch {}
133
+ }
134
+ if (_proxyDispatcherSet) return fetch(url, opts);
135
+
136
+ const { execFileSync } = require('child_process');
137
+ const args = ['-sS', '-L', '--max-time', '30'];
138
+ if (opts && opts.method) { args.push('-X', opts.method); }
139
+ if (opts && opts.headers) {
140
+ for (const [k, v] of Object.entries(opts.headers)) {
141
+ args.push('-H', `${k}: ${v}`);
142
+ }
143
+ }
144
+ if (opts && opts.body) { args.push('-d', typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body)); }
145
+ args.push(target);
146
+ try {
147
+ const stdout = execFileSync('curl', args, { encoding: 'utf8', timeout: 35000 });
148
+ const body = stdout.trim();
149
+ return {
150
+ ok: true,
151
+ status: 200,
152
+ statusText: 'OK',
153
+ text: async () => body,
154
+ json: async () => JSON.parse(body),
155
+ headers: new Map(),
156
+ };
157
+ } catch (e) {
158
+ const msg = e.stderr ? e.stderr.toString().trim() : e.message;
159
+ throw new Error(`Proxy curl failed: ${msg}`);
160
+ }
161
+ }
162
+
98
163
  // Try to load the full IntelligenceEngine
99
164
  let IntelligenceEngine = null;
100
165
  let engineAvailable = false;
@@ -363,7 +428,7 @@ class Intelligence {
363
428
  const server = new Server(
364
429
  {
365
430
  name: 'ruvector',
366
- version: '0.2.5',
431
+ version: '0.2.7',
367
432
  },
368
433
  {
369
434
  capabilities: {
@@ -3122,7 +3187,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3122
3187
  // Fetch from GCS if no fresh cache
3123
3188
  if (!manifest) {
3124
3189
  try {
3125
- const resp = await fetch(GCS_MANIFEST);
3190
+ const resp = await proxyFetch(GCS_MANIFEST, { signal: AbortSignal.timeout(15000) });
3126
3191
  if (resp.ok) {
3127
3192
  manifest = await resp.json();
3128
3193
  try {
@@ -3276,7 +3341,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3276
3341
  }
3277
3342
  }
3278
3343
 
3279
- // ── Brain Tool Handlers ──────────────────────────────────────────────
3344
+ // ── Brain Tool Handlers (direct fetch to pi.ruv.io) ─────────────────
3280
3345
  case 'brain_search':
3281
3346
  case 'brain_share':
3282
3347
  case 'brain_get':
@@ -3289,31 +3354,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3289
3354
  case 'brain_transfer':
3290
3355
  case 'brain_sync': {
3291
3356
  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
- });
3357
+ const brainUrl = process.env.BRAIN_URL || 'https://pi.ruv.io';
3358
+ const brainKey = process.env.PI;
3359
+ const hdrs = { 'Content-Type': 'application/json' };
3360
+ if (brainKey) hdrs['Authorization'] = `Bearer ${brainKey}`;
3297
3361
  const subCmd = name.replace('brain_', '');
3298
- let result;
3362
+ let url, fetchOpts = { headers: hdrs, signal: AbortSignal.timeout(30000) };
3299
3363
  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;
3364
+ case 'search': {
3365
+ const p = new URLSearchParams({ q: args.query || '' });
3366
+ if (args.category) p.set('category', args.category);
3367
+ if (args.limit) p.set('limit', String(args.limit));
3368
+ url = `${brainUrl}/v1/memories/search?${p}`;
3369
+ break;
3370
+ }
3371
+ case 'share': {
3372
+ url = `${brainUrl}/v1/memories`;
3373
+ fetchOpts.method = 'POST';
3374
+ 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 });
3375
+ break;
3376
+ }
3377
+ case 'get': url = `${brainUrl}/v1/memories/${args.id}`; break;
3378
+ case 'vote': {
3379
+ url = `${brainUrl}/v1/memories/${args.id}/vote`;
3380
+ fetchOpts.method = 'POST';
3381
+ fetchOpts.body = JSON.stringify({ direction: args.direction });
3382
+ break;
3383
+ }
3384
+ case 'list': {
3385
+ const p = new URLSearchParams();
3386
+ if (args.category) p.set('category', args.category);
3387
+ p.set('limit', String(args.limit || 20));
3388
+ if (args.offset) p.set('offset', String(args.offset));
3389
+ if (args.sort) p.set('sort', args.sort);
3390
+ if (args.tags) p.set('tags', args.tags);
3391
+ url = `${brainUrl}/v1/memories/list?${p}`;
3392
+ break;
3393
+ }
3394
+ case 'delete': {
3395
+ url = `${brainUrl}/v1/memories/${args.id}`;
3396
+ fetchOpts.method = 'DELETE';
3397
+ break;
3398
+ }
3399
+ case 'status': url = `${brainUrl}/v1/status`; break;
3400
+ case 'drift': {
3401
+ const p = new URLSearchParams();
3402
+ if (args.domain) p.set('domain', args.domain);
3403
+ url = `${brainUrl}/v1/drift?${p}`;
3404
+ break;
3405
+ }
3406
+ case 'partition': {
3407
+ const p = new URLSearchParams();
3408
+ if (args.domain) p.set('domain', args.domain);
3409
+ if (args.min_cluster_size) p.set('min_cluster_size', String(args.min_cluster_size));
3410
+ url = `${brainUrl}/v1/partition?${p}`;
3411
+ break;
3412
+ }
3413
+ case 'transfer': {
3414
+ url = `${brainUrl}/v1/transfer`;
3415
+ fetchOpts.method = 'POST';
3416
+ fetchOpts.body = JSON.stringify({ source_domain: args.source_domain, target_domain: args.target_domain });
3417
+ break;
3418
+ }
3419
+ case 'sync': url = `${brainUrl}/v1/lora/latest`; break;
3311
3420
  }
3421
+ const resp = await proxyFetch(url, fetchOpts);
3422
+ if (!resp.ok) {
3423
+ const errText = await resp.text().catch(() => resp.statusText);
3424
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `${resp.status} ${errText}` }, null, 2) }], isError: true };
3425
+ }
3426
+ const result = await resp.json();
3312
3427
  return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...result }, null, 2) }] };
3313
3428
  } 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
- }
3317
3429
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: e.message }, null, 2) }], isError: true };
3318
3430
  }
3319
3431
  }
@@ -3340,7 +3452,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3340
3452
  brain_flags: '/v1/status',
3341
3453
  };
3342
3454
  const endpoint = endpointMap[name];
3343
- const resp = await fetch(`${brainUrl}${endpoint}`, { headers: hdrs });
3455
+ const resp = await proxyFetch(`${brainUrl}${endpoint}`, { headers: hdrs, signal: AbortSignal.timeout(30000) });
3344
3456
  if (!resp.ok) {
3345
3457
  const errText = await resp.text().catch(() => resp.statusText);
3346
3458
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `${resp.status} ${errText}` }, null, 2) }], isError: true };
@@ -3384,7 +3496,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3384
3496
  const hdrs = { 'Content-Type': 'application/json' };
3385
3497
  if (brainKey) hdrs['Authorization'] = `Bearer ${brainKey}`;
3386
3498
 
3387
- const resp = await fetch(`${brainUrl}/v1/midstream`, { headers: hdrs });
3499
+ const resp = await proxyFetch(`${brainUrl}/v1/midstream`, { headers: hdrs, signal: AbortSignal.timeout(30000) });
3388
3500
  if (!resp.ok) {
3389
3501
  const errText = await resp.text().catch(() => resp.statusText);
3390
3502
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `${resp.status} ${errText}` }, null, 2) }], isError: true };
@@ -3414,7 +3526,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3414
3526
 
3415
3527
  async function timeFetch(url) {
3416
3528
  const start = performance.now();
3417
- const resp = await fetch(url, { headers: hdrs });
3529
+ const resp = await proxyFetch(url, { headers: hdrs });
3418
3530
  return { status: resp.status, elapsed: performance.now() - start };
3419
3531
  }
3420
3532
 
@@ -3467,7 +3579,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3467
3579
  if (brainKey) hdrs['Authorization'] = `Bearer ${brainKey}`;
3468
3580
  const limit = Math.min(Math.max(parseInt(args.limit) || 10, 1), 100);
3469
3581
  const q = encodeURIComponent(args.query);
3470
- const resp = await fetch(`${brainUrl}/v1/memories/search?q=${q}&limit=${limit}`, { headers: hdrs });
3582
+ const resp = await proxyFetch(`${brainUrl}/v1/memories/search?q=${q}&limit=${limit}`, { headers: hdrs, signal: AbortSignal.timeout(30000) });
3471
3583
  if (!resp.ok) {
3472
3584
  const errText = await resp.text().catch(() => resp.statusText);
3473
3585
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `${resp.status} ${errText}` }, null, 2) }], isError: true };
@@ -3487,8 +3599,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3487
3599
  if (brainKey) hdrs['Authorization'] = `Bearer ${brainKey}`;
3488
3600
 
3489
3601
  const [healthResp, midResp] = await Promise.all([
3490
- fetch(`${brainUrl}/v1/health`, { headers: hdrs }).then(r => r.json()).catch(e => ({ error: e.message })),
3491
- fetch(`${brainUrl}/v1/midstream`, { headers: hdrs }).then(r => r.json()).catch(e => ({ error: e.message })),
3602
+ proxyFetch(`${brainUrl}/v1/health`, { headers: hdrs, signal: AbortSignal.timeout(15000) }).then(r => r.json()).catch(e => ({ error: e.message })),
3603
+ proxyFetch(`${brainUrl}/v1/midstream`, { headers: hdrs, signal: AbortSignal.timeout(15000) }).then(r => r.json()).catch(e => ({ error: e.message })),
3492
3604
  ]);
3493
3605
 
3494
3606
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -3516,7 +3628,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3516
3628
  case 'balance': { const ps = process.env.PI ? require('crypto').createHash('shake256', { outputLength: 16 }).update(process.env.PI).digest('hex') : 'anonymous'; endpoint = `/balance/${ps}`; break; }
3517
3629
  case 'tasks': endpoint = `/tasks?limit=${args.limit || 20}`; break;
3518
3630
  }
3519
- const resp = await fetch(`${genesisUrl}${endpoint}`, {
3631
+ const resp = await proxyFetch(`${genesisUrl}${endpoint}`, {
3520
3632
  method,
3521
3633
  headers: { 'Content-Type': 'application/json', ...(process.env.PI ? { 'Authorization': `Bearer ${process.env.PI}` } : {}) },
3522
3634
  ...(body ? { body } : {})
@@ -3787,7 +3899,7 @@ async function main() {
3787
3899
  transport: 'sse',
3788
3900
  sessions: sessions.size,
3789
3901
  tools: 91,
3790
- version: '0.2.5'
3902
+ version: '0.2.7'
3791
3903
  }));
3792
3904
 
3793
3905
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvector",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "High-performance vector database for Node.js with automatic native/WASM fallback",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -80,12 +80,10 @@
80
80
  "typescript": "^5.3.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@ruvector/pi-brain": ">=0.1.0",
84
83
  "@ruvector/ruvllm": ">=2.0.0",
85
84
  "@ruvector/router": ">=0.1.0"
86
85
  },
87
86
  "peerDependenciesMeta": {
88
- "@ruvector/pi-brain": { "optional": true },
89
87
  "@ruvector/ruvllm": { "optional": true },
90
88
  "@ruvector/router": { "optional": true }
91
89
  },