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.
- package/bin/cli.js +528 -165
- package/bin/mcp-server.js +245 -26
- 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
|
-
//
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} catch
|
|
100
|
-
|
|
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
|
|
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
|
|
7134
|
-
const
|
|
7135
|
-
|
|
7136
|
-
{ name: '
|
|
7137
|
-
{ name: '
|
|
7138
|
-
{ name: '
|
|
7139
|
-
{ name: '
|
|
7140
|
-
{ name: '
|
|
7141
|
-
{ name: '
|
|
7142
|
-
{ name: '
|
|
7143
|
-
{ name: '
|
|
7144
|
-
{ name: '
|
|
7145
|
-
{ name: '
|
|
7146
|
-
{ name: '
|
|
7147
|
-
{ name: 'mcp_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
|
|
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(
|
|
7300
|
+
console.log(JSON.stringify({ version: manifest.version, count: examples.length, examples }, null, 2));
|
|
7190
7301
|
return;
|
|
7191
7302
|
}
|
|
7192
|
-
|
|
7193
|
-
console.log(chalk.
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
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
|
|
7206
|
-
.option('-a, --all', 'Download all
|
|
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
|
|
7210
|
-
const
|
|
7211
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
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
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
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
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
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
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
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(
|
|
7495
|
+
console.log(` ${chalk.red('FAIL')} ${fileName}: ${e.message}`);
|
|
7288
7496
|
fail++;
|
|
7289
7497
|
}
|
|
7290
7498
|
}
|
|
7291
|
-
|
|
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
|
-
.
|
|
7300
|
-
|
|
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.
|
|
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
|
|
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
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
{
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
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
|
|
3326
|
-
|
|
3327
|
-
|
|
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);
|