ruvector 0.2.1 → 0.2.2

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 +528 -165
  2. package/bin/mcp-server.js +245 -26
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -9,6 +9,52 @@ const chalk = _chalk.default || _chalk;
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
+ // Load .env from current directory (if exists)
13
+ try {
14
+ const envPath = path.join(process.cwd(), '.env');
15
+ if (fs.existsSync(envPath)) {
16
+ const envContent = fs.readFileSync(envPath, 'utf8');
17
+ for (const line of envContent.split('\n')) {
18
+ const trimmed = line.trim();
19
+ if (!trimmed || trimmed.startsWith('#')) continue;
20
+ const eqIdx = trimmed.indexOf('=');
21
+ if (eqIdx > 0) {
22
+ const key = trimmed.slice(0, eqIdx).trim();
23
+ let value = trimmed.slice(eqIdx + 1).trim();
24
+ // Strip surrounding quotes
25
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
26
+ value = value.slice(1, -1);
27
+ }
28
+ // Don't override existing env vars
29
+ if (!process.env[key]) {
30
+ process.env[key] = value;
31
+ }
32
+ }
33
+ }
34
+ }
35
+ } catch {}
36
+
37
+ // Load global config from ~/.ruvector/config.json (if exists)
38
+ try {
39
+ const os = require('os');
40
+ const configPath = path.join(os.homedir(), '.ruvector', 'config.json');
41
+ if (fs.existsSync(configPath)) {
42
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
43
+ // Map config keys to env vars (don't override existing)
44
+ const configMap = {
45
+ brain_url: 'BRAIN_URL',
46
+ pi_key: 'PI',
47
+ edge_genesis_url: 'EDGE_GENESIS_URL',
48
+ edge_relay_url: 'EDGE_RELAY_URL',
49
+ };
50
+ for (const [configKey, envKey] of Object.entries(configMap)) {
51
+ if (config[configKey] && !process.env[envKey]) {
52
+ process.env[envKey] = config[configKey];
53
+ }
54
+ }
55
+ }
56
+ } catch {}
57
+
12
58
  // Lazy load ora (spinner) - only needed for commands with progress indicators
13
59
  let _oraModule = null;
14
60
  function ora(text) {
@@ -45,59 +91,72 @@ function requireRuvector() {
45
91
  }
46
92
  }
47
93
 
48
- // Import GNN (optional - graceful fallback if not available)
94
+ // Lazy load GNN (optional - loaded on first use, not at startup)
95
+ let _gnnModule = undefined;
49
96
  let RuvectorLayer, TensorCompress, differentiableSearch, getCompressionLevel, hierarchicalForward;
50
97
  let gnnAvailable = false;
51
- try {
52
- const gnn = require('@ruvector/gnn');
53
- RuvectorLayer = gnn.RuvectorLayer;
54
- TensorCompress = gnn.TensorCompress;
55
- differentiableSearch = gnn.differentiableSearch;
56
- getCompressionLevel = gnn.getCompressionLevel;
57
- hierarchicalForward = gnn.hierarchicalForward;
58
- gnnAvailable = true;
59
- } catch (e) {
60
- // GNN not available - commands will show helpful message
98
+
99
+ function loadGnn() {
100
+ if (_gnnModule !== undefined) return _gnnModule;
101
+ try {
102
+ const gnn = require('@ruvector/gnn');
103
+ RuvectorLayer = gnn.RuvectorLayer;
104
+ TensorCompress = gnn.TensorCompress;
105
+ differentiableSearch = gnn.differentiableSearch;
106
+ getCompressionLevel = gnn.getCompressionLevel;
107
+ hierarchicalForward = gnn.hierarchicalForward;
108
+ _gnnModule = gnn;
109
+ gnnAvailable = true;
110
+ return gnn;
111
+ } catch {
112
+ _gnnModule = null;
113
+ gnnAvailable = false;
114
+ return null;
115
+ }
61
116
  }
62
117
 
63
- // Import Attention (optional - graceful fallback if not available)
118
+ // Lazy load Attention (optional - loaded on first use, not at startup)
119
+ let _attentionModule = undefined;
64
120
  let DotProductAttention, MultiHeadAttention, HyperbolicAttention, FlashAttention, LinearAttention, MoEAttention;
65
121
  let GraphRoPeAttention, EdgeFeaturedAttention, DualSpaceAttention, LocalGlobalAttention;
66
122
  let benchmarkAttention, computeAttentionAsync, batchAttentionCompute, parallelAttentionCompute;
67
123
  let expMap, logMap, mobiusAddition, poincareDistance, projectToPoincareBall;
68
124
  let attentionInfo, attentionVersion;
69
125
  let attentionAvailable = false;
70
- try {
71
- const attention = require('@ruvector/attention');
72
- // Core mechanisms
73
- DotProductAttention = attention.DotProductAttention;
74
- MultiHeadAttention = attention.MultiHeadAttention;
75
- HyperbolicAttention = attention.HyperbolicAttention;
76
- FlashAttention = attention.FlashAttention;
77
- LinearAttention = attention.LinearAttention;
78
- MoEAttention = attention.MoEAttention;
79
- // Graph attention
80
- GraphRoPeAttention = attention.GraphRoPeAttention;
81
- EdgeFeaturedAttention = attention.EdgeFeaturedAttention;
82
- DualSpaceAttention = attention.DualSpaceAttention;
83
- LocalGlobalAttention = attention.LocalGlobalAttention;
84
- // Utilities
85
- benchmarkAttention = attention.benchmarkAttention;
86
- computeAttentionAsync = attention.computeAttentionAsync;
87
- batchAttentionCompute = attention.batchAttentionCompute;
88
- parallelAttentionCompute = attention.parallelAttentionCompute;
89
- // Hyperbolic math
90
- expMap = attention.expMap;
91
- logMap = attention.logMap;
92
- mobiusAddition = attention.mobiusAddition;
93
- poincareDistance = attention.poincareDistance;
94
- projectToPoincareBall = attention.projectToPoincareBall;
95
- // Meta
96
- attentionInfo = attention.info;
97
- attentionVersion = attention.version;
98
- attentionAvailable = true;
99
- } catch (e) {
100
- // Attention not available - commands will show helpful message
126
+
127
+ function loadAttention() {
128
+ if (_attentionModule !== undefined) return _attentionModule;
129
+ try {
130
+ const attention = require('@ruvector/attention');
131
+ DotProductAttention = attention.DotProductAttention;
132
+ MultiHeadAttention = attention.MultiHeadAttention;
133
+ HyperbolicAttention = attention.HyperbolicAttention;
134
+ FlashAttention = attention.FlashAttention;
135
+ LinearAttention = attention.LinearAttention;
136
+ MoEAttention = attention.MoEAttention;
137
+ GraphRoPeAttention = attention.GraphRoPeAttention;
138
+ EdgeFeaturedAttention = attention.EdgeFeaturedAttention;
139
+ DualSpaceAttention = attention.DualSpaceAttention;
140
+ LocalGlobalAttention = attention.LocalGlobalAttention;
141
+ benchmarkAttention = attention.benchmarkAttention;
142
+ computeAttentionAsync = attention.computeAttentionAsync;
143
+ batchAttentionCompute = attention.batchAttentionCompute;
144
+ parallelAttentionCompute = attention.parallelAttentionCompute;
145
+ expMap = attention.expMap;
146
+ logMap = attention.logMap;
147
+ mobiusAddition = attention.mobiusAddition;
148
+ poincareDistance = attention.poincareDistance;
149
+ projectToPoincareBall = attention.projectToPoincareBall;
150
+ attentionInfo = attention.attentionInfo;
151
+ attentionVersion = attention.attentionVersion;
152
+ _attentionModule = attention;
153
+ attentionAvailable = true;
154
+ return attention;
155
+ } catch {
156
+ _attentionModule = null;
157
+ attentionAvailable = false;
158
+ return null;
159
+ }
101
160
  }
102
161
 
103
162
  const program = new Command();
@@ -369,7 +428,8 @@ program
369
428
 
370
429
  // Try to load ruvector for implementation info
371
430
  if (loadRuvector()) {
372
- const version = typeof getVersion === 'function' ? getVersion() : 'unknown';
431
+ const versionInfo = typeof getVersion === 'function' ? getVersion() : null;
432
+ const version = versionInfo && versionInfo.version ? versionInfo.version : 'unknown';
373
433
  const impl = typeof getImplementationType === 'function' ? getImplementationType() : 'native';
374
434
  console.log(chalk.white(` Core Version: ${chalk.yellow(version)}`));
375
435
  console.log(chalk.white(` Implementation: ${chalk.yellow(impl)}`));
@@ -377,6 +437,7 @@ program
377
437
  console.log(chalk.white(` Core: ${chalk.gray('Not loaded (install @ruvector/core)')}`));
378
438
  }
379
439
 
440
+ loadGnn();
380
441
  console.log(chalk.white(` GNN Module: ${gnnAvailable ? chalk.green('Available') : chalk.gray('Not installed')}`));
381
442
  console.log(chalk.white(` Node Version: ${chalk.yellow(process.version)}`));
382
443
  console.log(chalk.white(` Platform: ${chalk.yellow(process.platform)}`));
@@ -401,6 +462,7 @@ program
401
462
  const { execSync } = require('child_process');
402
463
 
403
464
  // Available optional packages - all ruvector npm packages
465
+ loadGnn();
404
466
  const availablePackages = {
405
467
  // Core packages
406
468
  core: {
@@ -689,6 +751,7 @@ program
689
751
 
690
752
  // Helper to check GNN availability
691
753
  function requireGnn() {
754
+ loadGnn();
692
755
  if (!gnnAvailable) {
693
756
  console.error(chalk.red('Error: GNN module not available.'));
694
757
  console.error(chalk.yellow('Install it with: npm install @ruvector/gnn'));
@@ -884,6 +947,7 @@ gnnCmd
884
947
  .command('info')
885
948
  .description('Show GNN module information')
886
949
  .action(() => {
950
+ loadGnn();
887
951
  if (!gnnAvailable) {
888
952
  console.log(chalk.yellow('\nGNN Module: Not installed'));
889
953
  console.log(chalk.white('Install with: npm install @ruvector/gnn'));
@@ -915,6 +979,7 @@ gnnCmd
915
979
 
916
980
  // Helper to require attention module
917
981
  function requireAttention() {
982
+ loadAttention();
918
983
  if (!attentionAvailable) {
919
984
  console.error(chalk.red('Error: @ruvector/attention is not installed'));
920
985
  console.error(chalk.yellow('Install it with: npm install @ruvector/attention'));
@@ -1242,6 +1307,7 @@ attentionCmd
1242
1307
  .command('info')
1243
1308
  .description('Show attention module information')
1244
1309
  .action(() => {
1310
+ loadAttention();
1245
1311
  if (!attentionAvailable) {
1246
1312
  console.log(chalk.yellow('\nAttention Module: Not installed'));
1247
1313
  console.log(chalk.white('Install with: npm install @ruvector/attention'));
@@ -1287,6 +1353,7 @@ attentionCmd
1287
1353
  .description('List all available attention mechanisms')
1288
1354
  .option('-v, --verbose', 'Show detailed information')
1289
1355
  .action((options) => {
1356
+ loadAttention();
1290
1357
  console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════════'));
1291
1358
  console.log(chalk.cyan(' Available Attention Mechanisms'));
1292
1359
  console.log(chalk.cyan('═══════════════════════════════════════════════════════════════\n'));
@@ -1429,6 +1496,7 @@ program
1429
1496
  }
1430
1497
 
1431
1498
  // Check @ruvector/gnn
1499
+ loadGnn();
1432
1500
  if (gnnAvailable) {
1433
1501
  console.log(chalk.green(` ✓ @ruvector/gnn installed`));
1434
1502
  } else {
@@ -1436,6 +1504,7 @@ program
1436
1504
  }
1437
1505
 
1438
1506
  // Check @ruvector/attention
1507
+ loadAttention();
1439
1508
  if (attentionAvailable) {
1440
1509
  console.log(chalk.green(` ✓ @ruvector/attention installed`));
1441
1510
  } else {
@@ -2531,6 +2600,7 @@ program
2531
2600
  }
2532
2601
 
2533
2602
  if (options.gnn) {
2603
+ loadGnn();
2534
2604
  if (!gnnAvailable) {
2535
2605
  console.log(chalk.yellow(' @ruvector/gnn not installed.'));
2536
2606
  console.log(chalk.white(' Install with: npm install @ruvector/gnn'));
@@ -7130,165 +7200,356 @@ rvfCmd.command('export <path>')
7130
7200
  } catch (e) { console.error(chalk.red(e.message)); process.exit(1); }
7131
7201
  });
7132
7202
 
7133
- // RVF example download/list commands
7134
- const RVF_EXAMPLES = [
7135
- { name: 'basic_store', size: '152 KB', desc: '1,000 vectors, dim 128, cosine metric' },
7136
- { name: 'semantic_search', size: '755 KB', desc: 'Semantic search with HNSW index' },
7137
- { name: 'rag_pipeline', size: '303 KB', desc: 'RAG pipeline with embeddings' },
7138
- { name: 'embedding_cache', size: '755 KB', desc: 'Cached embedding store' },
7139
- { name: 'quantization', size: '1.5 MB', desc: 'PQ-compressed vectors' },
7140
- { name: 'progressive_index', size: '2.5 MB', desc: 'Large-scale progressive HNSW index' },
7141
- { name: 'filtered_search', size: '255 KB', desc: 'Metadata-filtered vector search' },
7142
- { name: 'recommendation', size: '102 KB', desc: 'Recommendation engine vectors' },
7143
- { name: 'agent_memory', size: '32 KB', desc: 'AI agent episodic memory' },
7144
- { name: 'swarm_knowledge', size: '86 KB', desc: 'Multi-agent shared knowledge base' },
7145
- { name: 'experience_replay', size: '27 KB', desc: 'RL experience replay buffer' },
7146
- { name: 'tool_cache', size: '26 KB', desc: 'MCP tool call cache' },
7147
- { name: 'mcp_in_rvf', size: '32 KB', desc: 'MCP server embedded in RVF' },
7148
- { name: 'ruvbot', size: '51 KB', desc: 'Chatbot knowledge store' },
7149
- { name: 'claude_code_appliance', size: '17 KB', desc: 'Claude Code cognitive appliance' },
7150
- { name: 'lineage_parent', size: '52 KB', desc: 'COW parent file' },
7151
- { name: 'lineage_child', size: '26 KB', desc: 'COW child (derived) file' },
7152
- { name: 'self_booting', size: '31 KB', desc: 'Self-booting with KERNEL_SEG' },
7153
- { name: 'linux_microkernel', size: '15 KB', desc: 'Embedded Linux microkernel' },
7154
- { name: 'ebpf_accelerator', size: '153 KB', desc: 'eBPF distance accelerator' },
7155
- { name: 'browser_wasm', size: '14 KB', desc: 'Browser WASM module embedded' },
7156
- { name: 'tee_attestation', size: '102 KB', desc: 'TEE attestation with witnesses' },
7157
- { name: 'zero_knowledge', size: '52 KB', desc: 'ZK-proof witness chain' },
7158
- { name: 'sealed_engine', size: '208 KB', desc: 'Sealed inference engine' },
7159
- { name: 'access_control', size: '77 KB', desc: 'Permission-gated vectors' },
7160
- { name: 'financial_signals', size: '202 KB', desc: 'Financial signal vectors' },
7161
- { name: 'medical_imaging', size: '302 KB', desc: 'Medical imaging embeddings' },
7162
- { name: 'legal_discovery', size: '903 KB', desc: 'Legal document discovery' },
7163
- { name: 'multimodal_fusion', size: '804 KB', desc: 'Multi-modal embedding fusion' },
7164
- { name: 'hyperbolic_taxonomy', size: '23 KB', desc: 'Hyperbolic space taxonomy' },
7165
- { name: 'network_telemetry', size: '16 KB', desc: 'Network telemetry vectors' },
7166
- { name: 'postgres_bridge', size: '152 KB', desc: 'PostgreSQL bridge vectors' },
7167
- { name: 'ruvllm_inference', size: '133 KB', desc: 'RuvLLM inference cache' },
7168
- { name: 'serverless', size: '509 KB', desc: 'Serverless deployment bundle' },
7169
- { name: 'edge_iot', size: '27 KB', desc: 'Edge/IoT lightweight store' },
7170
- { name: 'dedup_detector', size: '153 KB', desc: 'Deduplication detector' },
7171
- { name: 'compacted', size: '77 KB', desc: 'Post-compaction example' },
7172
- { name: 'posix_fileops', size: '52 KB', desc: 'POSIX file operations test' },
7173
- { name: 'network_sync_a', size: '52 KB', desc: 'Network sync peer A' },
7174
- { name: 'network_sync_b', size: '52 KB', desc: 'Network sync peer B' },
7175
- { name: 'agent_handoff_a', size: '31 KB', desc: 'Agent handoff source' },
7176
- { name: 'agent_handoff_b', size: '11 KB', desc: 'Agent handoff target' },
7177
- { name: 'reasoning_parent', size: '5.6 KB', desc: 'Reasoning chain parent' },
7178
- { name: 'reasoning_child', size: '8.1 KB', desc: 'Reasoning chain child' },
7179
- { name: 'reasoning_grandchild', size: '162 B', desc: 'Minimal derived file' },
7203
+ // RVF example catalog - manifest-based with local cache + SHA-256 verification
7204
+ const BUILTIN_RVF_CATALOG = [
7205
+ // Minimal fallback if GCS and cache are both unavailable
7206
+ { name: 'basic_store', size_human: '152 KB', description: '1,000 vectors, dim 128, cosine metric', category: 'core' },
7207
+ { name: 'semantic_search', size_human: '755 KB', description: 'Semantic search with HNSW index', category: 'core' },
7208
+ { name: 'rag_pipeline', size_human: '303 KB', description: 'RAG pipeline with embeddings', category: 'core' },
7209
+ { name: 'agent_memory', size_human: '32 KB', description: 'AI agent episodic memory', category: 'ai' },
7210
+ { name: 'swarm_knowledge', size_human: '86 KB', description: 'Multi-agent shared knowledge base', category: 'ai' },
7211
+ { name: 'self_booting', size_human: '31 KB', description: 'Self-booting with KERNEL_SEG', category: 'compute' },
7212
+ { name: 'ebpf_accelerator', size_human: '153 KB', description: 'eBPF distance accelerator', category: 'compute' },
7213
+ { name: 'tee_attestation', size_human: '102 KB', description: 'TEE attestation with witnesses', category: 'security' },
7214
+ { name: 'claude_code_appliance', size_human: '17 KB', description: 'Claude Code cognitive appliance', category: 'integration' },
7215
+ { name: 'lineage_parent', size_human: '52 KB', description: 'COW parent file', category: 'lineage' },
7216
+ { name: 'financial_signals', size_human: '202 KB', description: 'Financial signal vectors', category: 'industry' },
7217
+ { name: 'mcp_in_rvf', size_human: '32 KB', description: 'MCP server embedded in RVF', category: 'integration' },
7180
7218
  ];
7181
7219
 
7182
- const RVF_BASE_URL = 'https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output';
7220
+ const GCS_MANIFEST_URL = 'https://storage.googleapis.com/ruvector-examples/manifest.json';
7221
+ const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output';
7222
+
7223
+ function getRvfCacheDir() {
7224
+ const os = require('os');
7225
+ return path.join(os.homedir(), '.ruvector', 'examples');
7226
+ }
7227
+
7228
+ async function getRvfManifest(opts = {}) {
7229
+ const cacheDir = getRvfCacheDir();
7230
+ const manifestPath = path.join(cacheDir, 'manifest.json');
7231
+
7232
+ // Check cache (1 hour TTL)
7233
+ if (!opts.refresh && fs.existsSync(manifestPath)) {
7234
+ try {
7235
+ const stat = fs.statSync(manifestPath);
7236
+ const age = Date.now() - stat.mtimeMs;
7237
+ if (age < 3600000) {
7238
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
7239
+ }
7240
+ } catch {}
7241
+ }
7242
+
7243
+ if (opts.offline) {
7244
+ // Offline mode - use cache even if stale
7245
+ if (fs.existsSync(manifestPath)) {
7246
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
7247
+ }
7248
+ return { examples: BUILTIN_RVF_CATALOG, base_url: GITHUB_RAW_BASE, version: 'builtin', offline: true };
7249
+ }
7250
+
7251
+ // Try GCS
7252
+ try {
7253
+ const resp = await fetch(GCS_MANIFEST_URL);
7254
+ if (resp.ok) {
7255
+ const manifest = await resp.json();
7256
+ fs.mkdirSync(cacheDir, { recursive: true });
7257
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
7258
+ return manifest;
7259
+ }
7260
+ } catch {}
7261
+
7262
+ // Fallback: stale cache
7263
+ if (fs.existsSync(manifestPath)) {
7264
+ try {
7265
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
7266
+ manifest._stale = true;
7267
+ return manifest;
7268
+ } catch {}
7269
+ }
7270
+
7271
+ // Final fallback: builtin catalog with GitHub URLs
7272
+ return { examples: BUILTIN_RVF_CATALOG, base_url: GITHUB_RAW_BASE, version: 'builtin' };
7273
+ }
7274
+
7275
+ function verifyRvfFile(filePath, expectedSha256) {
7276
+ if (!expectedSha256) return { verified: false, reason: 'No checksum available' };
7277
+ const crypto = require('crypto');
7278
+ const hash = crypto.createHash('sha256');
7279
+ const data = fs.readFileSync(filePath);
7280
+ hash.update(data);
7281
+ const actual = hash.digest('hex');
7282
+ return { verified: actual === expectedSha256, actual, expected: expectedSha256 };
7283
+ }
7183
7284
 
7184
7285
  rvfCmd.command('examples')
7185
- .description('List available example .rvf files')
7286
+ .description('List available example .rvf files from the catalog')
7287
+ .option('--category <cat>', 'Filter by category (core, ai, security, compute, lineage, industry, network, integration)')
7288
+ .option('--refresh', 'Force refresh manifest from server')
7289
+ .option('--offline', 'Use only cached data')
7186
7290
  .option('--json', 'Output as JSON')
7187
- .action((opts) => {
7291
+ .action(async (opts) => {
7292
+ const manifest = await getRvfManifest({ refresh: opts.refresh, offline: opts.offline });
7293
+ let examples = manifest.examples || [];
7294
+
7295
+ if (opts.category) {
7296
+ examples = examples.filter(e => e.category === opts.category);
7297
+ }
7298
+
7188
7299
  if (opts.json) {
7189
- console.log(JSON.stringify(RVF_EXAMPLES, null, 2));
7300
+ console.log(JSON.stringify({ version: manifest.version, count: examples.length, examples }, null, 2));
7190
7301
  return;
7191
7302
  }
7192
- console.log(chalk.bold.cyan('\nAvailable RVF Example Files (45 total)\n'));
7193
- console.log(chalk.dim(`Download: npx ruvector rvf download <name>\n`));
7194
- const maxName = Math.max(...RVF_EXAMPLES.map(e => e.name.length));
7195
- const maxSize = Math.max(...RVF_EXAMPLES.map(e => e.size.length));
7196
- for (const ex of RVF_EXAMPLES) {
7197
- const name = chalk.green(ex.name.padEnd(maxName));
7198
- const size = chalk.yellow(ex.size.padStart(maxSize));
7199
- console.log(` ${name} ${size} ${chalk.dim(ex.desc)}`);
7303
+
7304
+ console.log(chalk.bold.cyan(`\nRVF Example Files (${examples.length} of ${(manifest.examples || []).length} total)\n`));
7305
+ if (manifest._stale) console.log(chalk.yellow(' (Using stale cached manifest)\n'));
7306
+ if (manifest.version === 'builtin') console.log(chalk.yellow(' (Using built-in catalog -- run without --offline for full list)\n'));
7307
+ console.log(chalk.dim(` Download: npx ruvector rvf download <name>`));
7308
+ console.log(chalk.dim(` Filter: npx ruvector rvf examples --category ai\n`));
7309
+
7310
+ // Group by category
7311
+ const grouped = {};
7312
+ for (const ex of examples) {
7313
+ const cat = ex.category || 'other';
7314
+ if (!grouped[cat]) grouped[cat] = [];
7315
+ grouped[cat].push(ex);
7316
+ }
7317
+
7318
+ for (const [cat, items] of Object.entries(grouped).sort()) {
7319
+ const catDesc = manifest.categories ? manifest.categories[cat] || '' : '';
7320
+ console.log(chalk.bold.yellow(` ${cat} ${catDesc ? chalk.dim(`-- ${catDesc}`) : ''}`));
7321
+ for (const ex of items) {
7322
+ const name = chalk.green(ex.name.padEnd(28));
7323
+ const size = chalk.yellow((ex.size_human || '').padStart(8));
7324
+ console.log(` ${name} ${size} ${chalk.dim(ex.description || '')}`);
7325
+ }
7326
+ console.log();
7327
+ }
7328
+
7329
+ if (manifest.categories && !opts.category) {
7330
+ console.log(chalk.dim(` Categories: ${Object.keys(manifest.categories).join(', ')}\n`));
7200
7331
  }
7201
- console.log(chalk.dim(`\nFull catalog: https://github.com/ruvnet/ruvector/tree/main/examples/rvf/output\n`));
7202
7332
  });
7203
7333
 
7204
7334
  rvfCmd.command('download [names...]')
7205
- .description('Download example .rvf files from GitHub')
7206
- .option('-a, --all', 'Download all 45 examples (~11 MB)')
7335
+ .description('Download example .rvf files with integrity verification')
7336
+ .option('-a, --all', 'Download all examples')
7337
+ .option('-c, --category <cat>', 'Download all examples in a category')
7207
7338
  .option('-o, --output <dir>', 'Output directory', '.')
7339
+ .option('--verify', 'Re-verify cached files')
7340
+ .option('--no-cache', 'Skip cache, always download fresh')
7341
+ .option('--offline', 'Use only cached files')
7342
+ .option('--refresh', 'Refresh manifest before download')
7208
7343
  .action(async (names, opts) => {
7209
- const https = require('https');
7210
- const ALLOWED_REDIRECT_HOSTS = ['raw.githubusercontent.com', 'objects.githubusercontent.com', 'github.com'];
7211
- const sanitizeFileName = (name) => {
7212
- // Strip path separators and parent directory references
7213
- const base = path.basename(name);
7214
- // Only allow alphanumeric, underscores, hyphens, dots
7215
- if (!/^[\w\-.]+$/.test(base)) throw new Error(`Invalid filename: ${base}`);
7216
- return base;
7217
- };
7218
- const downloadFile = (url, dest) => new Promise((resolve, reject) => {
7219
- const file = fs.createWriteStream(dest);
7220
- https.get(url, (res) => {
7221
- if (res.statusCode === 302 || res.statusCode === 301) {
7222
- const redirectUrl = res.headers.location;
7223
- try {
7224
- const redirectHost = new URL(redirectUrl).hostname;
7225
- if (!ALLOWED_REDIRECT_HOSTS.includes(redirectHost)) {
7226
- file.close();
7227
- reject(new Error(`Redirect to untrusted host: ${redirectHost}`));
7228
- return;
7229
- }
7230
- } catch { file.close(); reject(new Error('Invalid redirect URL')); return; }
7231
- https.get(redirectUrl, (res2) => { res2.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', reject);
7232
- return;
7233
- }
7234
- if (res.statusCode !== 200) { file.close(); fs.unlinkSync(dest); reject(new Error(`HTTP ${res.statusCode}`)); return; }
7235
- res.pipe(file);
7236
- file.on('finish', () => { file.close(); resolve(); });
7237
- }).on('error', reject);
7238
- });
7344
+ const manifest = await getRvfManifest({ refresh: opts.refresh, offline: opts.offline });
7345
+ const examples = manifest.examples || [];
7346
+ const baseUrl = manifest.base_url || GITHUB_RAW_BASE;
7239
7347
 
7240
7348
  let toDownload = [];
7241
7349
  if (opts.all) {
7242
- toDownload = RVF_EXAMPLES.map(e => e.name);
7350
+ toDownload = examples;
7351
+ } else if (opts.category) {
7352
+ toDownload = examples.filter(e => e.category === opts.category);
7353
+ if (!toDownload.length) {
7354
+ console.error(chalk.red(`No examples in category '${opts.category}'`));
7355
+ process.exit(1);
7356
+ }
7243
7357
  } else if (names && names.length > 0) {
7244
- toDownload = names;
7358
+ for (const name of names) {
7359
+ const cleanName = name.replace(/\.rvf$/, '');
7360
+ const found = examples.find(e => e.name === cleanName);
7361
+ if (found) {
7362
+ toDownload.push(found);
7363
+ } else {
7364
+ console.error(chalk.red(`Unknown example: ${cleanName}. Run 'npx ruvector rvf examples' to list.`));
7365
+ }
7366
+ }
7367
+ if (!toDownload.length) process.exit(1);
7245
7368
  } else {
7246
- console.error(chalk.red('Specify example names or use --all. Run `npx ruvector rvf examples` to list.'));
7369
+ console.error(chalk.red('Specify example names, --all, or --category. Run `npx ruvector rvf examples` to list.'));
7247
7370
  process.exit(1);
7248
7371
  }
7249
7372
 
7250
7373
  const outDir = path.resolve(opts.output);
7251
7374
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
7375
+ const cacheDir = getRvfCacheDir();
7376
+ fs.mkdirSync(cacheDir, { recursive: true });
7252
7377
 
7253
7378
  console.log(chalk.bold.cyan(`\nDownloading ${toDownload.length} .rvf file(s) to ${outDir}\n`));
7254
- let ok = 0, fail = 0;
7255
- for (const name of toDownload) {
7256
- const rawName = name.endsWith('.rvf') ? name : `${name}.rvf`;
7257
- let fileName;
7258
- try { fileName = sanitizeFileName(rawName); } catch (e) {
7259
- console.log(chalk.red(`SKIPPED: ${e.message}`));
7379
+
7380
+ const https = require('https');
7381
+ const crypto = require('crypto');
7382
+ const ALLOWED_REDIRECT_HOSTS = ['raw.githubusercontent.com', 'objects.githubusercontent.com', 'github.com', 'storage.googleapis.com'];
7383
+
7384
+ const downloadFile = (url, dest) => new Promise((resolve, reject) => {
7385
+ const doGet = (getUrl) => {
7386
+ const mod = getUrl.startsWith('https') ? https : require('http');
7387
+ mod.get(getUrl, (res) => {
7388
+ if (res.statusCode === 301 || res.statusCode === 302) {
7389
+ const loc = res.headers.location;
7390
+ try {
7391
+ const host = new URL(loc).hostname;
7392
+ if (!ALLOWED_REDIRECT_HOSTS.includes(host)) {
7393
+ reject(new Error(`Redirect to untrusted host: ${host}`));
7394
+ return;
7395
+ }
7396
+ } catch { reject(new Error('Invalid redirect URL')); return; }
7397
+ doGet(loc);
7398
+ return;
7399
+ }
7400
+ if (res.statusCode !== 200) {
7401
+ reject(new Error(`HTTP ${res.statusCode}`));
7402
+ return;
7403
+ }
7404
+ const file = fs.createWriteStream(dest);
7405
+ res.pipe(file);
7406
+ file.on('finish', () => { file.close(); resolve(); });
7407
+ file.on('error', reject);
7408
+ }).on('error', reject);
7409
+ };
7410
+ doGet(url);
7411
+ });
7412
+
7413
+ let ok = 0, cached = 0, fail = 0, verified = 0;
7414
+
7415
+ for (const ex of toDownload) {
7416
+ const fileName = `${ex.name}.rvf`;
7417
+ // Sanitize filename
7418
+ if (!/^[\w\-.]+$/.test(fileName)) {
7419
+ console.log(` ${chalk.red('SKIP')} ${fileName} (invalid filename)`);
7260
7420
  fail++;
7261
7421
  continue;
7262
7422
  }
7263
- // Validate against known examples when not using --all
7264
- if (!opts.all) {
7265
- const baseName = fileName.replace(/\.rvf$/, '');
7266
- if (!RVF_EXAMPLES.some(e => e.name === baseName)) {
7267
- console.log(chalk.red(`SKIPPED: Unknown example '${baseName}'. Run 'npx ruvector rvf examples' to list.`));
7268
- fail++;
7423
+
7424
+ const destPath = path.join(outDir, fileName);
7425
+ const cachePath = path.join(cacheDir, fileName);
7426
+
7427
+ // Path containment check
7428
+ if (!path.resolve(destPath).startsWith(path.resolve(outDir))) {
7429
+ console.log(` ${chalk.red('SKIP')} ${fileName} (path traversal)`);
7430
+ fail++;
7431
+ continue;
7432
+ }
7433
+
7434
+ // Check cache first
7435
+ if (opts.cache !== false && fs.existsSync(cachePath) && !opts.verify) {
7436
+ // Verify if checksum available
7437
+ if (ex.sha256) {
7438
+ const check = verifyRvfFile(cachePath, ex.sha256);
7439
+ if (check.verified) {
7440
+ // Copy from cache
7441
+ if (path.resolve(destPath) !== path.resolve(cachePath)) {
7442
+ fs.copyFileSync(cachePath, destPath);
7443
+ }
7444
+ console.log(` ${chalk.green('CACHED')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')}`);
7445
+ cached++;
7446
+ continue;
7447
+ } else {
7448
+ // Cache corrupted, re-download
7449
+ console.log(` ${chalk.yellow('STALE')} ${fileName} -- re-downloading`);
7450
+ }
7451
+ } else {
7452
+ // Copy from cache (no checksum to verify)
7453
+ if (path.resolve(destPath) !== path.resolve(cachePath)) {
7454
+ fs.copyFileSync(cachePath, destPath);
7455
+ }
7456
+ console.log(` ${chalk.green('CACHED')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')}`);
7457
+ cached++;
7269
7458
  continue;
7270
7459
  }
7271
7460
  }
7272
- const url = `${RVF_BASE_URL}/${encodeURIComponent(fileName)}`;
7273
- const dest = path.join(outDir, fileName);
7274
- // Path containment check
7275
- if (!path.resolve(dest).startsWith(path.resolve(outDir) + path.sep) && path.resolve(dest) !== path.resolve(outDir)) {
7276
- console.log(chalk.red(`SKIPPED: Path traversal detected for '${fileName}'`));
7461
+
7462
+ if (opts.offline) {
7463
+ console.log(` ${chalk.yellow('SKIP')} ${fileName} (offline mode, not cached)`);
7277
7464
  fail++;
7278
7465
  continue;
7279
7466
  }
7467
+
7468
+ // Download
7469
+ const url = `${baseUrl}/${encodeURIComponent(fileName)}`;
7280
7470
  try {
7281
- process.stdout.write(chalk.dim(` ${fileName} ... `));
7282
- await downloadFile(url, dest);
7283
- const stat = fs.statSync(dest);
7284
- console.log(chalk.green(`OK (${(stat.size / 1024).toFixed(0)} KB)`));
7471
+ await downloadFile(url, cachePath);
7472
+
7473
+ // SHA-256 verify
7474
+ if (ex.sha256) {
7475
+ const check = verifyRvfFile(cachePath, ex.sha256);
7476
+ if (check.verified) {
7477
+ verified++;
7478
+ console.log(` ${chalk.green('OK')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')} ${chalk.green('SHA-256 verified')}`);
7479
+ } else {
7480
+ console.log(` ${chalk.red('FAIL')} ${fileName} -- SHA-256 mismatch! Expected ${ex.sha256.slice(0, 12)}... got ${check.actual.slice(0, 12)}...`);
7481
+ fs.unlinkSync(cachePath);
7482
+ fail++;
7483
+ continue;
7484
+ }
7485
+ } else {
7486
+ console.log(` ${chalk.green('OK')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')} ${chalk.yellow('(no checksum)')}`);
7487
+ }
7488
+
7489
+ // Copy to output dir if different from cache
7490
+ if (path.resolve(destPath) !== path.resolve(cachePath)) {
7491
+ fs.copyFileSync(cachePath, destPath);
7492
+ }
7285
7493
  ok++;
7286
7494
  } catch (e) {
7287
- console.log(chalk.red(`FAILED: ${e.message}`));
7495
+ console.log(` ${chalk.red('FAIL')} ${fileName}: ${e.message}`);
7288
7496
  fail++;
7289
7497
  }
7290
7498
  }
7291
- console.log(chalk.bold(`\nDone: ${ok} downloaded, ${fail} failed\n`));
7499
+
7500
+ console.log(chalk.bold(`\n Downloaded: ${ok}, Cached: ${cached}, Failed: ${fail}${verified ? `, Verified: ${verified}` : ''}\n`));
7501
+ });
7502
+
7503
+ // RVF cache management
7504
+ rvfCmd.command('cache <action>')
7505
+ .description('Manage local .rvf example cache (status, clear)')
7506
+ .action((action) => {
7507
+ const cacheDir = getRvfCacheDir();
7508
+
7509
+ switch (action) {
7510
+ case 'status': {
7511
+ if (!fs.existsSync(cacheDir)) {
7512
+ console.log(chalk.dim('\n No cache directory found.\n'));
7513
+ return;
7514
+ }
7515
+ const files = fs.readdirSync(cacheDir).filter(f => f.endsWith('.rvf'));
7516
+ const manifestExists = fs.existsSync(path.join(cacheDir, 'manifest.json'));
7517
+ let totalSize = 0;
7518
+ for (const f of files) {
7519
+ totalSize += fs.statSync(path.join(cacheDir, f)).size;
7520
+ }
7521
+ console.log(chalk.bold.cyan('\nRVF Cache Status\n'));
7522
+ console.log(` ${chalk.bold('Location:')} ${cacheDir}`);
7523
+ console.log(` ${chalk.bold('Files:')} ${files.length} .rvf files`);
7524
+ console.log(` ${chalk.bold('Size:')} ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
7525
+ console.log(` ${chalk.bold('Manifest:')} ${manifestExists ? chalk.green('cached') : chalk.dim('not cached')}`);
7526
+ if (manifestExists) {
7527
+ const stat = fs.statSync(path.join(cacheDir, 'manifest.json'));
7528
+ const age = Date.now() - stat.mtimeMs;
7529
+ const fresh = age < 3600000;
7530
+ console.log(` ${chalk.bold('Age:')} ${Math.floor(age / 60000)} min ${fresh ? chalk.green('(fresh)') : chalk.yellow('(stale)')}`);
7531
+ }
7532
+ console.log();
7533
+ break;
7534
+ }
7535
+ case 'clear': {
7536
+ if (!fs.existsSync(cacheDir)) {
7537
+ console.log(chalk.dim('\n No cache to clear.\n'));
7538
+ return;
7539
+ }
7540
+ const files = fs.readdirSync(cacheDir);
7541
+ let cleared = 0;
7542
+ for (const f of files) {
7543
+ fs.unlinkSync(path.join(cacheDir, f));
7544
+ cleared++;
7545
+ }
7546
+ console.log(chalk.green(`\n Cleared ${cleared} cached files from ${cacheDir}\n`));
7547
+ break;
7548
+ }
7549
+ default:
7550
+ console.error(chalk.red(`Unknown cache action: ${action}. Use: status, clear`));
7551
+ process.exit(1);
7552
+ }
7292
7553
  });
7293
7554
 
7294
7555
  // MCP Server command
@@ -7296,8 +7557,15 @@ const mcpCmd = program.command('mcp').description('MCP (Model Context Protocol)
7296
7557
 
7297
7558
  mcpCmd.command('start')
7298
7559
  .description('Start the RuVector MCP server')
7299
- .action(() => {
7300
- // Execute the mcp-server.js directly
7560
+ .option('-t, --transport <type>', 'Transport type: stdio or sse', 'stdio')
7561
+ .option('-p, --port <number>', 'Port for SSE transport', '8080')
7562
+ .option('--host <host>', 'Host to bind for SSE', '0.0.0.0')
7563
+ .action((opts) => {
7564
+ if (opts.transport === 'sse') {
7565
+ process.env.MCP_TRANSPORT = 'sse';
7566
+ process.env.MCP_PORT = opts.port;
7567
+ process.env.MCP_HOST = opts.host;
7568
+ }
7301
7569
  const mcpServerPath = path.join(__dirname, 'mcp-server.js');
7302
7570
  if (!fs.existsSync(mcpServerPath)) {
7303
7571
  console.error(chalk.red('Error: MCP server not found at'), mcpServerPath);
@@ -7813,6 +8081,98 @@ brainCmd.command('sync [direction]')
7813
8081
  } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7814
8082
  });
7815
8083
 
8084
+ brainCmd.command('page <action> [args...]')
8085
+ .description('Brainpedia page management (list, get, create, update, delete)')
8086
+ .option('--url <url>', 'Brain server URL')
8087
+ .option('--key <key>', 'Pi key')
8088
+ .option('--json', 'Output as JSON')
8089
+ .action(async (action, args, opts) => {
8090
+ const piBrain = await requirePiBrain();
8091
+ const config = getBrainConfig(opts);
8092
+ try {
8093
+ const client = new piBrain.PiBrainClient(config);
8094
+ let result;
8095
+ switch (action) {
8096
+ case 'list':
8097
+ result = await client.listPages ? client.listPages({ limit: 20 }) : { pages: [], message: 'Brainpedia not yet available on this server' };
8098
+ break;
8099
+ case 'get':
8100
+ if (!args[0]) { console.error(chalk.red('Usage: brain page get <slug>')); process.exit(1); }
8101
+ result = await client.getPage ? client.getPage(args[0]) : { error: 'Brainpedia not yet available' };
8102
+ break;
8103
+ case 'create':
8104
+ if (!args[0]) { console.error(chalk.red('Usage: brain page create <title> [--content <text>]')); process.exit(1); }
8105
+ result = await client.createPage ? client.createPage({ title: args[0], content: opts.content || '' }) : { error: 'Brainpedia not yet available' };
8106
+ break;
8107
+ case 'update':
8108
+ if (!args[0]) { console.error(chalk.red('Usage: brain page update <slug> [--content <text>]')); process.exit(1); }
8109
+ result = await client.updatePage ? client.updatePage(args[0], { content: opts.content || '' }) : { error: 'Brainpedia not yet available' };
8110
+ break;
8111
+ case 'delete':
8112
+ if (!args[0]) { console.error(chalk.red('Usage: brain page delete <slug>')); process.exit(1); }
8113
+ result = await client.deletePage ? client.deletePage(args[0]) : { error: 'Brainpedia not yet available' };
8114
+ break;
8115
+ default:
8116
+ console.error(chalk.red(`Unknown page action: ${action}. Use: list, get, create, update, delete`));
8117
+ process.exit(1);
8118
+ }
8119
+ if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8120
+ if (result.pages) {
8121
+ console.log(chalk.bold.cyan('\nBrainpedia Pages\n'));
8122
+ result.pages.forEach((p, i) => console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(p.title || p.slug)} ${chalk.dim(p.updated || '')}`));
8123
+ } else if (result.title) {
8124
+ console.log(chalk.bold.cyan(`\n${result.title}\n`));
8125
+ if (result.content) console.log(result.content);
8126
+ } else {
8127
+ console.log(JSON.stringify(result, null, 2));
8128
+ }
8129
+ console.log();
8130
+ } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8131
+ });
8132
+
8133
+ brainCmd.command('node <action> [args...]')
8134
+ .description('WASM compute node management (publish, list, status)')
8135
+ .option('--url <url>', 'Brain server URL')
8136
+ .option('--key <key>', 'Pi key')
8137
+ .option('--json', 'Output as JSON')
8138
+ .action(async (action, args, opts) => {
8139
+ const piBrain = await requirePiBrain();
8140
+ const config = getBrainConfig(opts);
8141
+ try {
8142
+ const client = new piBrain.PiBrainClient(config);
8143
+ let result;
8144
+ switch (action) {
8145
+ case 'publish':
8146
+ if (!args[0]) { console.error(chalk.red('Usage: brain node publish <wasm-file>')); process.exit(1); }
8147
+ const wasmPath = path.resolve(args[0]);
8148
+ if (!fs.existsSync(wasmPath)) { console.error(chalk.red(`File not found: ${wasmPath}`)); process.exit(1); }
8149
+ const wasmBytes = fs.readFileSync(wasmPath);
8150
+ result = await client.publishNode ? client.publishNode({ wasm: wasmBytes, name: path.basename(wasmPath, '.wasm') }) : { error: 'WASM node publish not yet available on this server' };
8151
+ break;
8152
+ case 'list':
8153
+ result = await client.listNodes ? client.listNodes({ limit: 20 }) : { nodes: [], message: 'WASM node listing not yet available' };
8154
+ break;
8155
+ case 'status':
8156
+ if (!args[0]) { console.error(chalk.red('Usage: brain node status <node-id>')); process.exit(1); }
8157
+ result = await client.nodeStatus ? client.nodeStatus(args[0]) : { error: 'WASM node status not yet available' };
8158
+ break;
8159
+ default:
8160
+ console.error(chalk.red(`Unknown node action: ${action}. Use: publish, list, status`));
8161
+ process.exit(1);
8162
+ }
8163
+ if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8164
+ if (result.nodes) {
8165
+ console.log(chalk.bold.cyan('\nWASM Compute Nodes\n'));
8166
+ result.nodes.forEach((n, i) => console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(n.name || n.id)} ${chalk.dim(n.status || '')}`));
8167
+ } else if (result.id) {
8168
+ console.log(chalk.green(`Published node: ${result.id}`));
8169
+ } else {
8170
+ console.log(JSON.stringify(result, null, 2));
8171
+ }
8172
+ console.log();
8173
+ } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8174
+ });
8175
+
7816
8176
  // ============================================================================
7817
8177
  // Edge Commands — Distributed compute via @ruvector/edge-net
7818
8178
  // ============================================================================
@@ -7934,14 +8294,17 @@ identityCmd.command('show')
7934
8294
  hash.update(piKey);
7935
8295
  const pseudonym = hash.digest('hex');
7936
8296
  const mcpToken = crypto.createHmac('sha256', piKey).update('mcp').digest('hex').slice(0, 32);
8297
+ const edgeKeyBuf = crypto.createHash('sha512').update(piKey).update('edge-net').digest().slice(0, 32);
8298
+ const edgeKey = edgeKeyBuf.toString('hex');
7937
8299
  if (opts.json || !process.stdout.isTTY) {
7938
- console.log(JSON.stringify({ pseudonym, mcp_token: mcpToken, key_prefix: piKey.slice(0, 8) + '...' }, null, 2));
8300
+ console.log(JSON.stringify({ pseudonym, mcp_token: mcpToken, edge_key: edgeKey, key_prefix: piKey.slice(0, 8) + '...' }, null, 2));
7939
8301
  return;
7940
8302
  }
7941
8303
  console.log(chalk.bold.cyan('\nPi Identity\n'));
7942
8304
  console.log(` ${chalk.bold('Key:')} ${piKey.slice(0, 8)}...${piKey.slice(-8)}`);
7943
8305
  console.log(` ${chalk.bold('Pseudonym:')} ${chalk.green(pseudonym)}`);
7944
8306
  console.log(` ${chalk.bold('MCP Token:')} ${chalk.dim(mcpToken)}`);
8307
+ console.log(` ${chalk.bold('Edge Key:')} ${chalk.dim(edgeKey)}`);
7945
8308
  console.log();
7946
8309
  });
7947
8310
 
package/bin/mcp-server.js CHANGED
@@ -363,7 +363,7 @@ class Intelligence {
363
363
  const server = new Server(
364
364
  {
365
365
  name: 'ruvector',
366
- version: '0.2.1',
366
+ version: '0.2.2',
367
367
  },
368
368
  {
369
369
  capabilities: {
@@ -1224,11 +1224,12 @@ const TOOLS = [
1224
1224
  },
1225
1225
  {
1226
1226
  name: 'rvf_examples',
1227
- description: 'List available example .rvf files with download URLs from the ruvector repository',
1227
+ description: 'List available example .rvf files with download URLs. Supports filtering by name, description, or category.',
1228
1228
  inputSchema: {
1229
1229
  type: 'object',
1230
1230
  properties: {
1231
- filter: { type: 'string', description: 'Filter examples by name or description substring' }
1231
+ filter: { type: 'string', description: 'Filter examples by name or description substring' },
1232
+ category: { type: 'string', description: 'Filter by category (core, ai, security, compute, lineage, industry, network, integration)' }
1232
1233
  },
1233
1234
  required: []
1234
1235
  }
@@ -3022,31 +3023,85 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3022
3023
  }
3023
3024
 
3024
3025
  case 'rvf_examples': {
3025
- const BASE_URL = 'https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output';
3026
- const examples = [
3027
- { name: 'basic_store', size: '152 KB', desc: '1,000 vectors, dim 128' },
3028
- { name: 'semantic_search', size: '755 KB', desc: 'Semantic search with HNSW' },
3029
- { name: 'rag_pipeline', size: '303 KB', desc: 'RAG pipeline embeddings' },
3030
- { name: 'agent_memory', size: '32 KB', desc: 'AI agent episodic memory' },
3031
- { name: 'swarm_knowledge', size: '86 KB', desc: 'Multi-agent knowledge base' },
3032
- { name: 'self_booting', size: '31 KB', desc: 'Self-booting with kernel' },
3033
- { name: 'ebpf_accelerator', size: '153 KB', desc: 'eBPF distance accelerator' },
3034
- { name: 'tee_attestation', size: '102 KB', desc: 'TEE attestation + witnesses' },
3035
- { name: 'lineage_parent', size: '52 KB', desc: 'COW parent file' },
3036
- { name: 'lineage_child', size: '26 KB', desc: 'COW child (derived)' },
3037
- { name: 'claude_code_appliance', size: '17 KB', desc: 'Claude Code appliance' },
3038
- { name: 'progressive_index', size: '2.5 MB', desc: 'Large-scale HNSW index' },
3039
- ];
3040
- let filtered = examples;
3026
+ const os = require('os');
3027
+ const GCS_MANIFEST = 'https://storage.googleapis.com/ruvector-examples/manifest.json';
3028
+ const GITHUB_RAW = 'https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output';
3029
+ const cacheDir = path.join(os.homedir(), '.ruvector', 'examples');
3030
+ const manifestPath = path.join(cacheDir, 'manifest.json');
3031
+
3032
+ let manifest;
3033
+ // Try cache first
3034
+ if (fs.existsSync(manifestPath)) {
3035
+ try {
3036
+ const stat = fs.statSync(manifestPath);
3037
+ if (Date.now() - stat.mtimeMs < 3600000) {
3038
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
3039
+ }
3040
+ } catch {}
3041
+ }
3042
+
3043
+ // Fetch from GCS if no fresh cache
3044
+ if (!manifest) {
3045
+ try {
3046
+ const resp = await fetch(GCS_MANIFEST);
3047
+ if (resp.ok) {
3048
+ manifest = await resp.json();
3049
+ try {
3050
+ fs.mkdirSync(cacheDir, { recursive: true });
3051
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3052
+ } catch {}
3053
+ }
3054
+ } catch {}
3055
+ }
3056
+
3057
+ // Fallback to hardcoded
3058
+ if (!manifest) {
3059
+ manifest = {
3060
+ version: 'builtin',
3061
+ base_url: GITHUB_RAW,
3062
+ examples: [
3063
+ { name: 'basic_store', size_human: '152 KB', description: '1,000 vectors, dim 128', category: 'core' },
3064
+ { name: 'semantic_search', size_human: '755 KB', description: 'Semantic search with HNSW', category: 'core' },
3065
+ { name: 'rag_pipeline', size_human: '303 KB', description: 'RAG pipeline embeddings', category: 'core' },
3066
+ { name: 'agent_memory', size_human: '32 KB', description: 'AI agent episodic memory', category: 'ai' },
3067
+ { name: 'swarm_knowledge', size_human: '86 KB', description: 'Multi-agent knowledge base', category: 'ai' },
3068
+ { name: 'self_booting', size_human: '31 KB', description: 'Self-booting with kernel', category: 'compute' },
3069
+ { name: 'ebpf_accelerator', size_human: '153 KB', description: 'eBPF distance accelerator', category: 'compute' },
3070
+ { name: 'tee_attestation', size_human: '102 KB', description: 'TEE attestation + witnesses', category: 'security' },
3071
+ { name: 'claude_code_appliance', size_human: '17 KB', description: 'Claude Code appliance', category: 'integration' },
3072
+ { name: 'lineage_parent', size_human: '52 KB', description: 'COW parent file', category: 'lineage' },
3073
+ { name: 'financial_signals', size_human: '202 KB', description: 'Financial signals', category: 'industry' },
3074
+ { name: 'progressive_index', size_human: '2.5 MB', description: 'Large-scale HNSW index', category: 'core' },
3075
+ ]
3076
+ };
3077
+ }
3078
+
3079
+ let examples = manifest.examples || [];
3080
+ const baseUrl = manifest.base_url || GITHUB_RAW;
3081
+
3041
3082
  if (args.filter) {
3042
3083
  const f = args.filter.toLowerCase();
3043
- filtered = examples.filter(e => e.name.includes(f) || e.desc.toLowerCase().includes(f));
3084
+ examples = examples.filter(e =>
3085
+ e.name.includes(f) ||
3086
+ (e.description || '').toLowerCase().includes(f) ||
3087
+ (e.category || '').includes(f)
3088
+ );
3089
+ }
3090
+
3091
+ if (args.category) {
3092
+ examples = examples.filter(e => e.category === args.category);
3044
3093
  }
3094
+
3045
3095
  return { content: [{ type: 'text', text: JSON.stringify({
3046
3096
  success: true,
3047
- total: 45,
3048
- shown: filtered.length,
3049
- examples: filtered.map(e => ({ ...e, url: `${BASE_URL}/${e.name}.rvf` })),
3097
+ version: manifest.version,
3098
+ total: (manifest.examples || []).length,
3099
+ shown: examples.length,
3100
+ examples: examples.map(e => ({
3101
+ ...e,
3102
+ url: `${baseUrl}/${e.name}.rvf`
3103
+ })),
3104
+ categories: manifest.categories || {},
3050
3105
  catalog: 'https://github.com/ruvnet/ruvector/tree/main/examples/rvf/output'
3051
3106
  }, null, 2) }] };
3052
3107
  }
@@ -3322,9 +3377,173 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3322
3377
 
3323
3378
  // Start server
3324
3379
  async function main() {
3325
- const transport = new StdioServerTransport();
3326
- await server.connect(transport);
3327
- console.error('RuVector MCP server running on stdio');
3380
+ const transportType = process.env.MCP_TRANSPORT || 'stdio';
3381
+
3382
+ if (transportType === 'sse') {
3383
+ const http = require('http');
3384
+ const crypto = require('crypto');
3385
+ const port = parseInt(process.env.MCP_PORT || '8080', 10);
3386
+ const host = process.env.MCP_HOST || '0.0.0.0';
3387
+
3388
+ // SSE MCP Transport Implementation
3389
+ // MCP over SSE uses:
3390
+ // GET /sse - SSE stream for server->client messages
3391
+ // POST /message - client->server JSON-RPC messages
3392
+
3393
+ const sessions = new Map();
3394
+
3395
+ const httpServer = http.createServer(async (req, res) => {
3396
+ // CORS headers
3397
+ res.setHeader('Access-Control-Allow-Origin', '*');
3398
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
3399
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
3400
+
3401
+ if (req.method === 'OPTIONS') {
3402
+ res.writeHead(204);
3403
+ res.end();
3404
+ return;
3405
+ }
3406
+
3407
+ const url = new URL(req.url, `http://${req.headers.host}`);
3408
+
3409
+ if (req.method === 'GET' && url.pathname === '/sse') {
3410
+ // SSE endpoint - establish persistent connection
3411
+ const sessionId = crypto.randomUUID();
3412
+
3413
+ res.writeHead(200, {
3414
+ 'Content-Type': 'text/event-stream',
3415
+ 'Cache-Control': 'no-cache',
3416
+ 'Connection': 'keep-alive',
3417
+ });
3418
+
3419
+ // Send endpoint event so client knows where to POST
3420
+ const displayHost = host === '0.0.0.0' ? 'localhost' : host;
3421
+ const messageUrl = `http://${displayHost}:${port}/message?sessionId=${sessionId}`;
3422
+ res.write(`event: endpoint\ndata: ${messageUrl}\n\n`);
3423
+
3424
+ // Store session
3425
+ sessions.set(sessionId, {
3426
+ res,
3427
+ messageQueue: [],
3428
+ });
3429
+
3430
+ // Create a custom transport for this session
3431
+ const sessionTransport = {
3432
+ _onMessage: null,
3433
+ _onClose: null,
3434
+ _onError: null,
3435
+ _started: false,
3436
+
3437
+ async start() {
3438
+ this._started = true;
3439
+ },
3440
+
3441
+ async close() {
3442
+ sessions.delete(sessionId);
3443
+ if (!res.writableEnded) {
3444
+ res.end();
3445
+ }
3446
+ },
3447
+
3448
+ async send(message) {
3449
+ if (!res.writableEnded) {
3450
+ res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
3451
+ }
3452
+ },
3453
+
3454
+ set onmessage(handler) { this._onMessage = handler; },
3455
+ get onmessage() { return this._onMessage; },
3456
+ set onclose(handler) { this._onClose = handler; },
3457
+ get onclose() { return this._onClose; },
3458
+ set onerror(handler) { this._onError = handler; },
3459
+ get onerror() { return this._onError; },
3460
+ };
3461
+
3462
+ sessions.get(sessionId).transport = sessionTransport;
3463
+
3464
+ // Connect server to this transport
3465
+ await server.connect(sessionTransport);
3466
+
3467
+ // Process any queued messages
3468
+ const session = sessions.get(sessionId);
3469
+ if (session) {
3470
+ for (const msg of session.messageQueue) {
3471
+ if (sessionTransport._onMessage) {
3472
+ sessionTransport._onMessage(msg);
3473
+ }
3474
+ }
3475
+ session.messageQueue = [];
3476
+ }
3477
+
3478
+ // Handle disconnect
3479
+ req.on('close', () => {
3480
+ sessions.delete(sessionId);
3481
+ if (sessionTransport._onClose) {
3482
+ sessionTransport._onClose();
3483
+ }
3484
+ });
3485
+
3486
+ } else if (req.method === 'POST' && url.pathname === '/message') {
3487
+ // Message endpoint - receive client JSON-RPC messages
3488
+ const sessionId = url.searchParams.get('sessionId');
3489
+ const session = sessions.get(sessionId);
3490
+
3491
+ if (!session) {
3492
+ res.writeHead(404, { 'Content-Type': 'application/json' });
3493
+ res.end(JSON.stringify({ error: 'Session not found' }));
3494
+ return;
3495
+ }
3496
+
3497
+ let body = '';
3498
+ req.on('data', chunk => { body += chunk; });
3499
+ req.on('end', () => {
3500
+ try {
3501
+ const message = JSON.parse(body);
3502
+
3503
+ if (session.transport && session.transport._onMessage) {
3504
+ session.transport._onMessage(message);
3505
+ } else {
3506
+ session.messageQueue.push(message);
3507
+ }
3508
+
3509
+ res.writeHead(202, { 'Content-Type': 'application/json' });
3510
+ res.end(JSON.stringify({ status: 'accepted' }));
3511
+ } catch (e) {
3512
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3513
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
3514
+ }
3515
+ });
3516
+
3517
+ } else if (req.method === 'GET' && url.pathname === '/health') {
3518
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3519
+ res.end(JSON.stringify({
3520
+ status: 'ok',
3521
+ transport: 'sse',
3522
+ sessions: sessions.size,
3523
+ tools: 91,
3524
+ version: '0.2.2'
3525
+ }));
3526
+
3527
+ } else {
3528
+ res.writeHead(404, { 'Content-Type': 'application/json' });
3529
+ res.end(JSON.stringify({ error: 'Not found. Use GET /sse for SSE stream, POST /message for JSON-RPC, GET /health for status.' }));
3530
+ }
3531
+ });
3532
+
3533
+ httpServer.listen(port, host, () => {
3534
+ const displayHost = host === '0.0.0.0' ? 'localhost' : host;
3535
+ console.error(`RuVector MCP server running on SSE at http://${host}:${port}`);
3536
+ console.error(` SSE endpoint: http://${displayHost}:${port}/sse`);
3537
+ console.error(` Message endpoint: http://${displayHost}:${port}/message`);
3538
+ console.error(` Health check: http://${displayHost}:${port}/health`);
3539
+ });
3540
+
3541
+ } else {
3542
+ // Default: stdio transport
3543
+ const transport = new StdioServerTransport();
3544
+ await server.connect(transport);
3545
+ console.error('RuVector MCP server running on stdio');
3546
+ }
3328
3547
  }
3329
3548
 
3330
3549
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvector",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",