ruvector 0.2.18 → 0.2.20

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 CHANGED
@@ -9,52 +9,6 @@ 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
-
58
12
  // Lazy load ora (spinner) - only needed for commands with progress indicators
59
13
  let _oraModule = null;
60
14
  function ora(text) {
@@ -92,7 +46,8 @@ function requireRuvector() {
92
46
  }
93
47
 
94
48
  // Lazy load GNN (optional - loaded on first use, not at startup)
95
- let _gnnModule = undefined;
49
+ // Saves ~6ms startup time by deferring require('@ruvector/gnn')
50
+ let _gnnModule = undefined; // undefined = not yet attempted, null = failed, object = loaded
96
51
  let RuvectorLayer, TensorCompress, differentiableSearch, getCompressionLevel, hierarchicalForward;
97
52
  let gnnAvailable = false;
98
53
 
@@ -108,88 +63,16 @@ function loadGnn() {
108
63
  _gnnModule = gnn;
109
64
  gnnAvailable = true;
110
65
  return gnn;
111
- } catch {
66
+ } catch (e) {
112
67
  _gnnModule = null;
113
68
  gnnAvailable = false;
114
69
  return null;
115
70
  }
116
71
  }
117
72
 
118
- // ── Proxy-aware fetch wrapper ───────────────────────────────────────────────
119
- // Detects HTTP_PROXY / HTTPS_PROXY / ALL_PROXY env vars and routes traffic
120
- // through the proxy when configured. Falls back to native fetch() when no
121
- // proxy is set or the target matches NO_PROXY.
122
- let _proxyDispatcherSet = false;
123
-
124
- function getProxyUrl(targetUrl) {
125
- const NO_PROXY = process.env.NO_PROXY || process.env.no_proxy || '';
126
- if (NO_PROXY === '*') return null;
127
- if (NO_PROXY) {
128
- try {
129
- const host = new URL(targetUrl).hostname.toLowerCase();
130
- const patterns = NO_PROXY.split(',').map(p => p.trim().toLowerCase());
131
- for (const p of patterns) {
132
- if (!p) continue;
133
- if (host === p || host.endsWith(p.startsWith('.') ? p : '.' + p)) return null;
134
- }
135
- } catch {}
136
- }
137
- return process.env.HTTPS_PROXY || process.env.https_proxy
138
- || process.env.HTTP_PROXY || process.env.http_proxy
139
- || process.env.ALL_PROXY || process.env.all_proxy
140
- || null;
141
- }
142
-
143
- async function proxyFetch(url, opts) {
144
- const target = typeof url === 'string' ? url : url.toString();
145
- const proxy = getProxyUrl(target);
146
- if (!proxy) return fetch(url, opts);
147
-
148
- // Try undici ProxyAgent (bundled in Node 18+)
149
- if (!_proxyDispatcherSet) {
150
- try {
151
- const undici = require('undici');
152
- if (undici.ProxyAgent && undici.setGlobalDispatcher) {
153
- undici.setGlobalDispatcher(new undici.ProxyAgent(proxy));
154
- _proxyDispatcherSet = true;
155
- }
156
- } catch {}
157
- }
158
- if (_proxyDispatcherSet) return fetch(url, opts);
159
-
160
- // Fallback: shell out to curl (which natively respects proxy env vars)
161
- const { execFileSync } = require('child_process');
162
- const args = ['-sS', '-L', '--max-time', '30', '-w', '\n%{http_code}'];
163
- if (opts && opts.method) { args.push('-X', opts.method); }
164
- if (opts && opts.headers) {
165
- for (const [k, v] of Object.entries(opts.headers)) {
166
- args.push('-H', `${k}: ${v}`);
167
- }
168
- }
169
- if (opts && opts.body) { args.push('-d', typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body)); }
170
- args.push(target);
171
- try {
172
- const stdout = execFileSync('curl', args, { encoding: 'utf8', timeout: 35000 });
173
- const lines = stdout.trimEnd().split('\n');
174
- const statusCode = parseInt(lines.pop(), 10) || 200;
175
- const body = lines.join('\n').trim();
176
- const ok = statusCode >= 200 && statusCode < 300;
177
- return {
178
- ok,
179
- status: statusCode,
180
- statusText: ok ? 'OK' : `HTTP ${statusCode}`,
181
- text: async () => body,
182
- json: async () => body ? JSON.parse(body) : {},
183
- headers: new Map(),
184
- };
185
- } catch (e) {
186
- const msg = e.stderr ? e.stderr.toString().trim() : e.message;
187
- throw new Error(`Proxy curl failed: ${msg}`);
188
- }
189
- }
190
-
191
73
  // Lazy load Attention (optional - loaded on first use, not at startup)
192
- let _attentionModule = undefined;
74
+ // Saves ~5ms startup time by deferring require('@ruvector/attention')
75
+ let _attentionModule = undefined; // undefined = not yet attempted
193
76
  let DotProductAttention, MultiHeadAttention, HyperbolicAttention, FlashAttention, LinearAttention, MoEAttention;
194
77
  let GraphRoPeAttention, EdgeFeaturedAttention, DualSpaceAttention, LocalGlobalAttention;
195
78
  let benchmarkAttention, computeAttentionAsync, batchAttentionCompute, parallelAttentionCompute;
@@ -201,31 +84,36 @@ function loadAttention() {
201
84
  if (_attentionModule !== undefined) return _attentionModule;
202
85
  try {
203
86
  const attention = require('@ruvector/attention');
87
+ // Core mechanisms
204
88
  DotProductAttention = attention.DotProductAttention;
205
89
  MultiHeadAttention = attention.MultiHeadAttention;
206
90
  HyperbolicAttention = attention.HyperbolicAttention;
207
91
  FlashAttention = attention.FlashAttention;
208
92
  LinearAttention = attention.LinearAttention;
209
93
  MoEAttention = attention.MoEAttention;
94
+ // Graph attention
210
95
  GraphRoPeAttention = attention.GraphRoPeAttention;
211
96
  EdgeFeaturedAttention = attention.EdgeFeaturedAttention;
212
97
  DualSpaceAttention = attention.DualSpaceAttention;
213
98
  LocalGlobalAttention = attention.LocalGlobalAttention;
99
+ // Utilities
214
100
  benchmarkAttention = attention.benchmarkAttention;
215
101
  computeAttentionAsync = attention.computeAttentionAsync;
216
102
  batchAttentionCompute = attention.batchAttentionCompute;
217
103
  parallelAttentionCompute = attention.parallelAttentionCompute;
104
+ // Hyperbolic math
218
105
  expMap = attention.expMap;
219
106
  logMap = attention.logMap;
220
107
  mobiusAddition = attention.mobiusAddition;
221
108
  poincareDistance = attention.poincareDistance;
222
109
  projectToPoincareBall = attention.projectToPoincareBall;
223
- attentionInfo = attention.attentionInfo;
224
- attentionVersion = attention.attentionVersion;
110
+ // Meta
111
+ attentionInfo = attention.info;
112
+ attentionVersion = attention.version;
225
113
  _attentionModule = attention;
226
114
  attentionAvailable = true;
227
115
  return attention;
228
- } catch {
116
+ } catch (e) {
229
117
  _attentionModule = null;
230
118
  attentionAvailable = false;
231
119
  return null;
@@ -256,13 +144,11 @@ program
256
144
  try {
257
145
  const dimension = parseInt(options.dimension);
258
146
  const db = new VectorDB({
259
- dimension,
147
+ dimensions: dimension,
260
148
  metric: options.metric,
261
- path: dbPath,
262
- autoPersist: true
149
+ storagePath: dbPath,
263
150
  });
264
151
 
265
- db.save(dbPath);
266
152
  spinner.succeed(chalk.green(`Database created: ${dbPath}`));
267
153
  console.log(chalk.gray(` Dimension: ${dimension}`));
268
154
  console.log(chalk.gray(` Metric: ${options.metric}`));
@@ -434,7 +320,7 @@ program
434
320
  let spinner = ora('Creating database...').start();
435
321
 
436
322
  try {
437
- const db = new VectorDB({ dimension, metric: 'cosine' });
323
+ const db = new VectorDB({ dimensions: dimension, metric: 'cosine' });
438
324
  spinner.succeed();
439
325
 
440
326
  // Insert benchmark
@@ -478,10 +364,9 @@ program
478
364
  console.log(chalk.gray(` Avg Latency: ${chalk.yellow(avgLatency)}ms`));
479
365
 
480
366
  // Stats
481
- const stats = db.stats();
482
367
  console.log(chalk.cyan('\nFinal Stats:'));
483
- console.log(chalk.white(` Vector Count: ${chalk.yellow(stats.count)}`));
484
- console.log(chalk.white(` Dimension: ${chalk.yellow(stats.dimension)}`));
368
+ console.log(chalk.white(` Vector Count: ${chalk.yellow(numVectors)}`));
369
+ console.log(chalk.white(` Dimension: ${chalk.yellow(dimension)}`));
485
370
  console.log(chalk.white(` Implementation: ${chalk.yellow(getImplementationType())}`));
486
371
 
487
372
  } catch (error) {
@@ -496,13 +381,16 @@ program
496
381
  .command('info')
497
382
  .description('Show ruvector information')
498
383
  .action(() => {
384
+ // Trigger lazy load of optional modules for availability check
385
+ loadGnn();
386
+ loadAttention();
387
+
499
388
  console.log(chalk.cyan('\nruvector Information'));
500
389
  console.log(chalk.white(` CLI Version: ${chalk.yellow(packageJson.version)}`));
501
390
 
502
391
  // Try to load ruvector for implementation info
503
392
  if (loadRuvector()) {
504
- const versionInfo = typeof getVersion === 'function' ? getVersion() : null;
505
- const version = versionInfo && versionInfo.version ? versionInfo.version : 'unknown';
393
+ const version = typeof getVersion === 'function' ? getVersion() : 'unknown';
506
394
  const impl = typeof getImplementationType === 'function' ? getImplementationType() : 'native';
507
395
  console.log(chalk.white(` Core Version: ${chalk.yellow(version)}`));
508
396
  console.log(chalk.white(` Implementation: ${chalk.yellow(impl)}`));
@@ -510,7 +398,6 @@ program
510
398
  console.log(chalk.white(` Core: ${chalk.gray('Not loaded (install @ruvector/core)')}`));
511
399
  }
512
400
 
513
- loadGnn();
514
401
  console.log(chalk.white(` GNN Module: ${gnnAvailable ? chalk.green('Available') : chalk.gray('Not installed')}`));
515
402
  console.log(chalk.white(` Node Version: ${chalk.yellow(process.version)}`));
516
403
  console.log(chalk.white(` Platform: ${chalk.yellow(process.platform)}`));
@@ -534,8 +421,10 @@ program
534
421
  .action(async (packages, options) => {
535
422
  const { execSync } = require('child_process');
536
423
 
537
- // Available optional packages - all ruvector npm packages
424
+ // Trigger lazy load to check availability
538
425
  loadGnn();
426
+
427
+ // Available optional packages - all ruvector npm packages
539
428
  const availablePackages = {
540
429
  // Core packages
541
430
  core: {
@@ -822,7 +711,7 @@ program
822
711
  // GNN Commands
823
712
  // =============================================================================
824
713
 
825
- // Helper to check GNN availability
714
+ // Helper to check GNN availability (triggers lazy load)
826
715
  function requireGnn() {
827
716
  loadGnn();
828
717
  if (!gnnAvailable) {
@@ -1050,7 +939,7 @@ gnnCmd
1050
939
  // Attention Commands
1051
940
  // =============================================================================
1052
941
 
1053
- // Helper to require attention module
942
+ // Helper to require attention module (triggers lazy load)
1054
943
  function requireAttention() {
1055
944
  loadAttention();
1056
945
  if (!attentionAvailable) {
@@ -1483,6 +1372,10 @@ program
1483
1372
  .action(async (options) => {
1484
1373
  const { execSync } = require('child_process');
1485
1374
 
1375
+ // Trigger lazy load of optional modules for availability check
1376
+ loadGnn();
1377
+ loadAttention();
1378
+
1486
1379
  console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════════'));
1487
1380
  console.log(chalk.cyan(' RuVector Doctor'));
1488
1381
  console.log(chalk.cyan('═══════════════════════════════════════════════════════════════\n'));
@@ -1569,7 +1462,6 @@ program
1569
1462
  }
1570
1463
 
1571
1464
  // Check @ruvector/gnn
1572
- loadGnn();
1573
1465
  if (gnnAvailable) {
1574
1466
  console.log(chalk.green(` ✓ @ruvector/gnn installed`));
1575
1467
  } else {
@@ -1577,7 +1469,6 @@ program
1577
1469
  }
1578
1470
 
1579
1471
  // Check @ruvector/attention
1580
- loadAttention();
1581
1472
  if (attentionAvailable) {
1582
1473
  console.log(chalk.green(` ✓ @ruvector/attention installed`));
1583
1474
  } else {
@@ -2643,7 +2534,7 @@ program
2643
2534
  const spinner = ora('Creating demo database...').start();
2644
2535
 
2645
2536
  try {
2646
- const db = new VectorDB({ dimension: 4, metric: 'cosine' });
2537
+ const db = new VectorDB({ dimensions: 4, metric: 'cosine' });
2647
2538
 
2648
2539
  spinner.text = 'Inserting vectors...';
2649
2540
  db.insert('vec1', [1.0, 0.0, 0.0, 0.0], { label: 'x-axis' });
@@ -3551,29 +3442,15 @@ hooksCmd.command('init')
3551
3442
  }
3552
3443
 
3553
3444
  // MCP server configuration (unless --minimal or --no-mcp)
3554
- // Note: We only use enabledMcpjsonServers (not mcpServers) to avoid
3555
- // Claude Code regenerating .mcp.json and stripping user-added fields
3556
- // like autoStart. See: https://github.com/ruvnet/RuVector/issues/250
3557
3445
  if (!opts.minimal && opts.mcp !== false) {
3558
- // Only reference servers via enabledMcpjsonServers — never write mcpServers: {}
3559
- // which can trigger Claude Code to regenerate .mcp.json
3560
- const mcpJsonPath = path.join(process.cwd(), '.mcp.json');
3561
- let mcpJson = {};
3562
- if (fs.existsSync(mcpJsonPath)) {
3563
- try { mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')); } catch {}
3564
- }
3565
- // Only add to enabledMcpjsonServers if the server is defined in .mcp.json
3566
- if (mcpJson.mcpServers?.['claude-flow'] || mcpJson.mcpServers?.['ruvector']) {
3567
- const serverName = mcpJson.mcpServers['ruvector'] ? 'ruvector' : 'claude-flow';
3568
- if (!settings.enabledMcpjsonServers?.includes(serverName)) {
3569
- settings.enabledMcpjsonServers = settings.enabledMcpjsonServers || [];
3570
- settings.enabledMcpjsonServers.push(serverName);
3446
+ settings.mcpServers = settings.mcpServers || {};
3447
+ // Only add if not already configured
3448
+ if (!settings.mcpServers['claude-flow'] && !settings.enabledMcpjsonServers?.includes('claude-flow')) {
3449
+ settings.enabledMcpjsonServers = settings.enabledMcpjsonServers || [];
3450
+ if (!settings.enabledMcpjsonServers.includes('claude-flow')) {
3451
+ settings.enabledMcpjsonServers.push('claude-flow');
3571
3452
  }
3572
3453
  }
3573
- // Remove stale mcpServers: {} that could trigger .mcp.json regeneration
3574
- if (settings.mcpServers && Object.keys(settings.mcpServers).length === 0) {
3575
- delete settings.mcpServers;
3576
- }
3577
3454
  console.log(chalk.blue(' ✓ MCP servers configured'));
3578
3455
  }
3579
3456
 
@@ -4193,7 +4070,7 @@ hooksCmd.command('suggest-context').description('Suggest relevant context').acti
4193
4070
  console.log(`RuVector Intelligence: ${stats.total_patterns} learned patterns, ${stats.total_errors} error fixes available. Use 'ruvector hooks route' for agent suggestions.`);
4194
4071
  });
4195
4072
 
4196
- hooksCmd.command('remember').description('Store in memory').option('-t, --type <type>', 'Memory type', 'general').option('--silent', 'Suppress output').option('--semantic', 'Use ONNX semantic embeddings (slower, better quality)').argument('<content...>', 'Content').action(async (content, opts) => {
4073
+ hooksCmd.command('remember').description('Store in memory').requiredOption('-t, --type <type>', 'Memory type').option('--silent', 'Suppress output').option('--semantic', 'Use ONNX semantic embeddings (slower, better quality)').argument('<content...>', 'Content').action(async (content, opts) => {
4197
4074
  const intel = new Intelligence();
4198
4075
  let id;
4199
4076
  if (opts.semantic) {
@@ -5783,16 +5660,6 @@ hooksCmd.command('doctor')
5783
5660
  };
5784
5661
  Object.values(settings.hooks || {}).forEach(checkCommands);
5785
5662
 
5786
- // Remove empty mcpServers that can cause .mcp.json overwrites (#250)
5787
- if (settings.mcpServers && Object.keys(settings.mcpServers).length === 0) {
5788
- if (opts.fix) {
5789
- delete settings.mcpServers;
5790
- fixes.push('Removed empty mcpServers to prevent .mcp.json overwrites');
5791
- } else {
5792
- issues.push({ severity: 'warning', message: 'Empty mcpServers in settings.json can cause .mcp.json overwrites', fix: 'Will remove empty mcpServers' });
5793
- }
5794
- }
5795
-
5796
5663
  // Save fixes
5797
5664
  if (opts.fix && fixes.length > 0) {
5798
5665
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -7297,356 +7164,165 @@ rvfCmd.command('export <path>')
7297
7164
  } catch (e) { console.error(chalk.red(e.message)); process.exit(1); }
7298
7165
  });
7299
7166
 
7300
- // RVF example catalog - manifest-based with local cache + SHA-256 verification
7301
- const BUILTIN_RVF_CATALOG = [
7302
- // Minimal fallback if GCS and cache are both unavailable
7303
- { name: 'basic_store', size_human: '152 KB', description: '1,000 vectors, dim 128, cosine metric', category: 'core' },
7304
- { name: 'semantic_search', size_human: '755 KB', description: 'Semantic search with HNSW index', category: 'core' },
7305
- { name: 'rag_pipeline', size_human: '303 KB', description: 'RAG pipeline with embeddings', category: 'core' },
7306
- { name: 'agent_memory', size_human: '32 KB', description: 'AI agent episodic memory', category: 'ai' },
7307
- { name: 'swarm_knowledge', size_human: '86 KB', description: 'Multi-agent shared knowledge base', category: 'ai' },
7308
- { name: 'self_booting', size_human: '31 KB', description: 'Self-booting with KERNEL_SEG', category: 'compute' },
7309
- { name: 'ebpf_accelerator', size_human: '153 KB', description: 'eBPF distance accelerator', category: 'compute' },
7310
- { name: 'tee_attestation', size_human: '102 KB', description: 'TEE attestation with witnesses', category: 'security' },
7311
- { name: 'claude_code_appliance', size_human: '17 KB', description: 'Claude Code cognitive appliance', category: 'integration' },
7312
- { name: 'lineage_parent', size_human: '52 KB', description: 'COW parent file', category: 'lineage' },
7313
- { name: 'financial_signals', size_human: '202 KB', description: 'Financial signal vectors', category: 'industry' },
7314
- { name: 'mcp_in_rvf', size_human: '32 KB', description: 'MCP server embedded in RVF', category: 'integration' },
7167
+ // RVF example download/list commands
7168
+ const RVF_EXAMPLES = [
7169
+ { name: 'basic_store', size: '152 KB', desc: '1,000 vectors, dim 128, cosine metric' },
7170
+ { name: 'semantic_search', size: '755 KB', desc: 'Semantic search with HNSW index' },
7171
+ { name: 'rag_pipeline', size: '303 KB', desc: 'RAG pipeline with embeddings' },
7172
+ { name: 'embedding_cache', size: '755 KB', desc: 'Cached embedding store' },
7173
+ { name: 'quantization', size: '1.5 MB', desc: 'PQ-compressed vectors' },
7174
+ { name: 'progressive_index', size: '2.5 MB', desc: 'Large-scale progressive HNSW index' },
7175
+ { name: 'filtered_search', size: '255 KB', desc: 'Metadata-filtered vector search' },
7176
+ { name: 'recommendation', size: '102 KB', desc: 'Recommendation engine vectors' },
7177
+ { name: 'agent_memory', size: '32 KB', desc: 'AI agent episodic memory' },
7178
+ { name: 'swarm_knowledge', size: '86 KB', desc: 'Multi-agent shared knowledge base' },
7179
+ { name: 'experience_replay', size: '27 KB', desc: 'RL experience replay buffer' },
7180
+ { name: 'tool_cache', size: '26 KB', desc: 'MCP tool call cache' },
7181
+ { name: 'mcp_in_rvf', size: '32 KB', desc: 'MCP server embedded in RVF' },
7182
+ { name: 'ruvbot', size: '51 KB', desc: 'Chatbot knowledge store' },
7183
+ { name: 'claude_code_appliance', size: '17 KB', desc: 'Claude Code cognitive appliance' },
7184
+ { name: 'lineage_parent', size: '52 KB', desc: 'COW parent file' },
7185
+ { name: 'lineage_child', size: '26 KB', desc: 'COW child (derived) file' },
7186
+ { name: 'self_booting', size: '31 KB', desc: 'Self-booting with KERNEL_SEG' },
7187
+ { name: 'linux_microkernel', size: '15 KB', desc: 'Embedded Linux microkernel' },
7188
+ { name: 'ebpf_accelerator', size: '153 KB', desc: 'eBPF distance accelerator' },
7189
+ { name: 'browser_wasm', size: '14 KB', desc: 'Browser WASM module embedded' },
7190
+ { name: 'tee_attestation', size: '102 KB', desc: 'TEE attestation with witnesses' },
7191
+ { name: 'zero_knowledge', size: '52 KB', desc: 'ZK-proof witness chain' },
7192
+ { name: 'sealed_engine', size: '208 KB', desc: 'Sealed inference engine' },
7193
+ { name: 'access_control', size: '77 KB', desc: 'Permission-gated vectors' },
7194
+ { name: 'financial_signals', size: '202 KB', desc: 'Financial signal vectors' },
7195
+ { name: 'medical_imaging', size: '302 KB', desc: 'Medical imaging embeddings' },
7196
+ { name: 'legal_discovery', size: '903 KB', desc: 'Legal document discovery' },
7197
+ { name: 'multimodal_fusion', size: '804 KB', desc: 'Multi-modal embedding fusion' },
7198
+ { name: 'hyperbolic_taxonomy', size: '23 KB', desc: 'Hyperbolic space taxonomy' },
7199
+ { name: 'network_telemetry', size: '16 KB', desc: 'Network telemetry vectors' },
7200
+ { name: 'postgres_bridge', size: '152 KB', desc: 'PostgreSQL bridge vectors' },
7201
+ { name: 'ruvllm_inference', size: '133 KB', desc: 'RuvLLM inference cache' },
7202
+ { name: 'serverless', size: '509 KB', desc: 'Serverless deployment bundle' },
7203
+ { name: 'edge_iot', size: '27 KB', desc: 'Edge/IoT lightweight store' },
7204
+ { name: 'dedup_detector', size: '153 KB', desc: 'Deduplication detector' },
7205
+ { name: 'compacted', size: '77 KB', desc: 'Post-compaction example' },
7206
+ { name: 'posix_fileops', size: '52 KB', desc: 'POSIX file operations test' },
7207
+ { name: 'network_sync_a', size: '52 KB', desc: 'Network sync peer A' },
7208
+ { name: 'network_sync_b', size: '52 KB', desc: 'Network sync peer B' },
7209
+ { name: 'agent_handoff_a', size: '31 KB', desc: 'Agent handoff source' },
7210
+ { name: 'agent_handoff_b', size: '11 KB', desc: 'Agent handoff target' },
7211
+ { name: 'reasoning_parent', size: '5.6 KB', desc: 'Reasoning chain parent' },
7212
+ { name: 'reasoning_child', size: '8.1 KB', desc: 'Reasoning chain child' },
7213
+ { name: 'reasoning_grandchild', size: '162 B', desc: 'Minimal derived file' },
7315
7214
  ];
7316
7215
 
7317
- const GCS_MANIFEST_URL = 'https://storage.googleapis.com/ruvector-examples/manifest.json';
7318
- const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output';
7319
-
7320
- function getRvfCacheDir() {
7321
- const os = require('os');
7322
- return path.join(os.homedir(), '.ruvector', 'examples');
7323
- }
7324
-
7325
- async function getRvfManifest(opts = {}) {
7326
- const cacheDir = getRvfCacheDir();
7327
- const manifestPath = path.join(cacheDir, 'manifest.json');
7328
-
7329
- // Check cache (1 hour TTL)
7330
- if (!opts.refresh && fs.existsSync(manifestPath)) {
7331
- try {
7332
- const stat = fs.statSync(manifestPath);
7333
- const age = Date.now() - stat.mtimeMs;
7334
- if (age < 3600000) {
7335
- return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
7336
- }
7337
- } catch {}
7338
- }
7339
-
7340
- if (opts.offline) {
7341
- // Offline mode - use cache even if stale
7342
- if (fs.existsSync(manifestPath)) {
7343
- return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
7344
- }
7345
- return { examples: BUILTIN_RVF_CATALOG, base_url: GITHUB_RAW_BASE, version: 'builtin', offline: true };
7346
- }
7347
-
7348
- // Try GCS
7349
- try {
7350
- const resp = await proxyFetch(GCS_MANIFEST_URL, { signal: AbortSignal.timeout(15000) });
7351
- if (resp.ok) {
7352
- const manifest = await resp.json();
7353
- fs.mkdirSync(cacheDir, { recursive: true });
7354
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
7355
- return manifest;
7356
- }
7357
- } catch {}
7358
-
7359
- // Fallback: stale cache
7360
- if (fs.existsSync(manifestPath)) {
7361
- try {
7362
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
7363
- manifest._stale = true;
7364
- return manifest;
7365
- } catch {}
7366
- }
7367
-
7368
- // Final fallback: builtin catalog with GitHub URLs
7369
- return { examples: BUILTIN_RVF_CATALOG, base_url: GITHUB_RAW_BASE, version: 'builtin' };
7370
- }
7371
-
7372
- function verifyRvfFile(filePath, expectedSha256) {
7373
- if (!expectedSha256) return { verified: false, reason: 'No checksum available' };
7374
- const crypto = require('crypto');
7375
- const hash = crypto.createHash('sha256');
7376
- const data = fs.readFileSync(filePath);
7377
- hash.update(data);
7378
- const actual = hash.digest('hex');
7379
- return { verified: actual === expectedSha256, actual, expected: expectedSha256 };
7380
- }
7216
+ const RVF_BASE_URL = 'https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output';
7381
7217
 
7382
7218
  rvfCmd.command('examples')
7383
- .description('List available example .rvf files from the catalog')
7384
- .option('--category <cat>', 'Filter by category (core, ai, security, compute, lineage, industry, network, integration)')
7385
- .option('--refresh', 'Force refresh manifest from server')
7386
- .option('--offline', 'Use only cached data')
7219
+ .description('List available example .rvf files')
7387
7220
  .option('--json', 'Output as JSON')
7388
- .action(async (opts) => {
7389
- const manifest = await getRvfManifest({ refresh: opts.refresh, offline: opts.offline });
7390
- let examples = manifest.examples || [];
7391
-
7392
- if (opts.category) {
7393
- examples = examples.filter(e => e.category === opts.category);
7394
- }
7395
-
7221
+ .action((opts) => {
7396
7222
  if (opts.json) {
7397
- console.log(JSON.stringify({ version: manifest.version, count: examples.length, examples }, null, 2));
7223
+ console.log(JSON.stringify(RVF_EXAMPLES, null, 2));
7398
7224
  return;
7399
7225
  }
7400
-
7401
- console.log(chalk.bold.cyan(`\nRVF Example Files (${examples.length} of ${(manifest.examples || []).length} total)\n`));
7402
- if (manifest._stale) console.log(chalk.yellow(' (Using stale cached manifest)\n'));
7403
- if (manifest.version === 'builtin') console.log(chalk.yellow(' (Using built-in catalog -- run without --offline for full list)\n'));
7404
- console.log(chalk.dim(` Download: npx ruvector rvf download <name>`));
7405
- console.log(chalk.dim(` Filter: npx ruvector rvf examples --category ai\n`));
7406
-
7407
- // Group by category
7408
- const grouped = {};
7409
- for (const ex of examples) {
7410
- const cat = ex.category || 'other';
7411
- if (!grouped[cat]) grouped[cat] = [];
7412
- grouped[cat].push(ex);
7413
- }
7414
-
7415
- for (const [cat, items] of Object.entries(grouped).sort()) {
7416
- const catDesc = manifest.categories ? manifest.categories[cat] || '' : '';
7417
- console.log(chalk.bold.yellow(` ${cat} ${catDesc ? chalk.dim(`-- ${catDesc}`) : ''}`));
7418
- for (const ex of items) {
7419
- const name = chalk.green(ex.name.padEnd(28));
7420
- const size = chalk.yellow((ex.size_human || '').padStart(8));
7421
- console.log(` ${name} ${size} ${chalk.dim(ex.description || '')}`);
7422
- }
7423
- console.log();
7424
- }
7425
-
7426
- if (manifest.categories && !opts.category) {
7427
- console.log(chalk.dim(` Categories: ${Object.keys(manifest.categories).join(', ')}\n`));
7226
+ console.log(chalk.bold.cyan('\nAvailable RVF Example Files (45 total)\n'));
7227
+ console.log(chalk.dim(`Download: npx ruvector rvf download <name>\n`));
7228
+ const maxName = Math.max(...RVF_EXAMPLES.map(e => e.name.length));
7229
+ const maxSize = Math.max(...RVF_EXAMPLES.map(e => e.size.length));
7230
+ for (const ex of RVF_EXAMPLES) {
7231
+ const name = chalk.green(ex.name.padEnd(maxName));
7232
+ const size = chalk.yellow(ex.size.padStart(maxSize));
7233
+ console.log(` ${name} ${size} ${chalk.dim(ex.desc)}`);
7428
7234
  }
7235
+ console.log(chalk.dim(`\nFull catalog: https://github.com/ruvnet/ruvector/tree/main/examples/rvf/output\n`));
7429
7236
  });
7430
7237
 
7431
7238
  rvfCmd.command('download [names...]')
7432
- .description('Download example .rvf files with integrity verification')
7433
- .option('-a, --all', 'Download all examples')
7434
- .option('-c, --category <cat>', 'Download all examples in a category')
7239
+ .description('Download example .rvf files from GitHub')
7240
+ .option('-a, --all', 'Download all 45 examples (~11 MB)')
7435
7241
  .option('-o, --output <dir>', 'Output directory', '.')
7436
- .option('--verify', 'Re-verify cached files')
7437
- .option('--no-cache', 'Skip cache, always download fresh')
7438
- .option('--offline', 'Use only cached files')
7439
- .option('--refresh', 'Refresh manifest before download')
7440
7242
  .action(async (names, opts) => {
7441
- const manifest = await getRvfManifest({ refresh: opts.refresh, offline: opts.offline });
7442
- const examples = manifest.examples || [];
7443
- const baseUrl = manifest.base_url || GITHUB_RAW_BASE;
7243
+ const https = require('https');
7244
+ const ALLOWED_REDIRECT_HOSTS = ['raw.githubusercontent.com', 'objects.githubusercontent.com', 'github.com'];
7245
+ const sanitizeFileName = (name) => {
7246
+ // Strip path separators and parent directory references
7247
+ const base = path.basename(name);
7248
+ // Only allow alphanumeric, underscores, hyphens, dots
7249
+ if (!/^[\w\-.]+$/.test(base)) throw new Error(`Invalid filename: ${base}`);
7250
+ return base;
7251
+ };
7252
+ const downloadFile = (url, dest) => new Promise((resolve, reject) => {
7253
+ const file = fs.createWriteStream(dest);
7254
+ https.get(url, (res) => {
7255
+ if (res.statusCode === 302 || res.statusCode === 301) {
7256
+ const redirectUrl = res.headers.location;
7257
+ try {
7258
+ const redirectHost = new URL(redirectUrl).hostname;
7259
+ if (!ALLOWED_REDIRECT_HOSTS.includes(redirectHost)) {
7260
+ file.close();
7261
+ reject(new Error(`Redirect to untrusted host: ${redirectHost}`));
7262
+ return;
7263
+ }
7264
+ } catch { file.close(); reject(new Error('Invalid redirect URL')); return; }
7265
+ https.get(redirectUrl, (res2) => { res2.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', reject);
7266
+ return;
7267
+ }
7268
+ if (res.statusCode !== 200) { file.close(); fs.unlinkSync(dest); reject(new Error(`HTTP ${res.statusCode}`)); return; }
7269
+ res.pipe(file);
7270
+ file.on('finish', () => { file.close(); resolve(); });
7271
+ }).on('error', reject);
7272
+ });
7444
7273
 
7445
7274
  let toDownload = [];
7446
7275
  if (opts.all) {
7447
- toDownload = examples;
7448
- } else if (opts.category) {
7449
- toDownload = examples.filter(e => e.category === opts.category);
7450
- if (!toDownload.length) {
7451
- console.error(chalk.red(`No examples in category '${opts.category}'`));
7452
- process.exit(1);
7453
- }
7276
+ toDownload = RVF_EXAMPLES.map(e => e.name);
7454
7277
  } else if (names && names.length > 0) {
7455
- for (const name of names) {
7456
- const cleanName = name.replace(/\.rvf$/, '');
7457
- const found = examples.find(e => e.name === cleanName);
7458
- if (found) {
7459
- toDownload.push(found);
7460
- } else {
7461
- console.error(chalk.red(`Unknown example: ${cleanName}. Run 'npx ruvector rvf examples' to list.`));
7462
- }
7463
- }
7464
- if (!toDownload.length) process.exit(1);
7278
+ toDownload = names;
7465
7279
  } else {
7466
- console.error(chalk.red('Specify example names, --all, or --category. Run `npx ruvector rvf examples` to list.'));
7280
+ console.error(chalk.red('Specify example names or use --all. Run `npx ruvector rvf examples` to list.'));
7467
7281
  process.exit(1);
7468
7282
  }
7469
7283
 
7470
7284
  const outDir = path.resolve(opts.output);
7471
7285
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
7472
- const cacheDir = getRvfCacheDir();
7473
- fs.mkdirSync(cacheDir, { recursive: true });
7474
7286
 
7475
7287
  console.log(chalk.bold.cyan(`\nDownloading ${toDownload.length} .rvf file(s) to ${outDir}\n`));
7476
-
7477
- const https = require('https');
7478
- const crypto = require('crypto');
7479
- const ALLOWED_REDIRECT_HOSTS = ['raw.githubusercontent.com', 'objects.githubusercontent.com', 'github.com', 'storage.googleapis.com'];
7480
-
7481
- const downloadFile = (url, dest) => new Promise((resolve, reject) => {
7482
- const doGet = (getUrl) => {
7483
- const mod = getUrl.startsWith('https') ? https : require('http');
7484
- mod.get(getUrl, (res) => {
7485
- if (res.statusCode === 301 || res.statusCode === 302) {
7486
- const loc = res.headers.location;
7487
- try {
7488
- const host = new URL(loc).hostname;
7489
- if (!ALLOWED_REDIRECT_HOSTS.includes(host)) {
7490
- reject(new Error(`Redirect to untrusted host: ${host}`));
7491
- return;
7492
- }
7493
- } catch { reject(new Error('Invalid redirect URL')); return; }
7494
- doGet(loc);
7495
- return;
7496
- }
7497
- if (res.statusCode !== 200) {
7498
- reject(new Error(`HTTP ${res.statusCode}`));
7499
- return;
7500
- }
7501
- const file = fs.createWriteStream(dest);
7502
- res.pipe(file);
7503
- file.on('finish', () => { file.close(); resolve(); });
7504
- file.on('error', reject);
7505
- }).on('error', reject);
7506
- };
7507
- doGet(url);
7508
- });
7509
-
7510
- let ok = 0, cached = 0, fail = 0, verified = 0;
7511
-
7512
- for (const ex of toDownload) {
7513
- const fileName = `${ex.name}.rvf`;
7514
- // Sanitize filename
7515
- if (!/^[\w\-.]+$/.test(fileName)) {
7516
- console.log(` ${chalk.red('SKIP')} ${fileName} (invalid filename)`);
7517
- fail++;
7518
- continue;
7519
- }
7520
-
7521
- const destPath = path.join(outDir, fileName);
7522
- const cachePath = path.join(cacheDir, fileName);
7523
-
7524
- // Path containment check
7525
- if (!path.resolve(destPath).startsWith(path.resolve(outDir))) {
7526
- console.log(` ${chalk.red('SKIP')} ${fileName} (path traversal)`);
7288
+ let ok = 0, fail = 0;
7289
+ for (const name of toDownload) {
7290
+ const rawName = name.endsWith('.rvf') ? name : `${name}.rvf`;
7291
+ let fileName;
7292
+ try { fileName = sanitizeFileName(rawName); } catch (e) {
7293
+ console.log(chalk.red(`SKIPPED: ${e.message}`));
7527
7294
  fail++;
7528
7295
  continue;
7529
7296
  }
7530
-
7531
- // Check cache first
7532
- if (opts.cache !== false && fs.existsSync(cachePath) && !opts.verify) {
7533
- // Verify if checksum available
7534
- if (ex.sha256) {
7535
- const check = verifyRvfFile(cachePath, ex.sha256);
7536
- if (check.verified) {
7537
- // Copy from cache
7538
- if (path.resolve(destPath) !== path.resolve(cachePath)) {
7539
- fs.copyFileSync(cachePath, destPath);
7540
- }
7541
- console.log(` ${chalk.green('CACHED')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')}`);
7542
- cached++;
7543
- continue;
7544
- } else {
7545
- // Cache corrupted, re-download
7546
- console.log(` ${chalk.yellow('STALE')} ${fileName} -- re-downloading`);
7547
- }
7548
- } else {
7549
- // Copy from cache (no checksum to verify)
7550
- if (path.resolve(destPath) !== path.resolve(cachePath)) {
7551
- fs.copyFileSync(cachePath, destPath);
7552
- }
7553
- console.log(` ${chalk.green('CACHED')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')}`);
7554
- cached++;
7297
+ // Validate against known examples when not using --all
7298
+ if (!opts.all) {
7299
+ const baseName = fileName.replace(/\.rvf$/, '');
7300
+ if (!RVF_EXAMPLES.some(e => e.name === baseName)) {
7301
+ console.log(chalk.red(`SKIPPED: Unknown example '${baseName}'. Run 'npx ruvector rvf examples' to list.`));
7302
+ fail++;
7555
7303
  continue;
7556
7304
  }
7557
7305
  }
7558
-
7559
- if (opts.offline) {
7560
- console.log(` ${chalk.yellow('SKIP')} ${fileName} (offline mode, not cached)`);
7306
+ const url = `${RVF_BASE_URL}/${encodeURIComponent(fileName)}`;
7307
+ const dest = path.join(outDir, fileName);
7308
+ // Path containment check
7309
+ if (!path.resolve(dest).startsWith(path.resolve(outDir) + path.sep) && path.resolve(dest) !== path.resolve(outDir)) {
7310
+ console.log(chalk.red(`SKIPPED: Path traversal detected for '${fileName}'`));
7561
7311
  fail++;
7562
7312
  continue;
7563
7313
  }
7564
-
7565
- // Download
7566
- const url = `${baseUrl}/${encodeURIComponent(fileName)}`;
7567
7314
  try {
7568
- await downloadFile(url, cachePath);
7569
-
7570
- // SHA-256 verify
7571
- if (ex.sha256) {
7572
- const check = verifyRvfFile(cachePath, ex.sha256);
7573
- if (check.verified) {
7574
- verified++;
7575
- console.log(` ${chalk.green('OK')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')} ${chalk.green('SHA-256 verified')}`);
7576
- } else {
7577
- console.log(` ${chalk.red('FAIL')} ${fileName} -- SHA-256 mismatch! Expected ${ex.sha256.slice(0, 12)}... got ${check.actual.slice(0, 12)}...`);
7578
- fs.unlinkSync(cachePath);
7579
- fail++;
7580
- continue;
7581
- }
7582
- } else {
7583
- console.log(` ${chalk.green('OK')} ${chalk.cyan(fileName)} ${chalk.dim(ex.size_human || '')} ${chalk.yellow('(no checksum)')}`);
7584
- }
7585
-
7586
- // Copy to output dir if different from cache
7587
- if (path.resolve(destPath) !== path.resolve(cachePath)) {
7588
- fs.copyFileSync(cachePath, destPath);
7589
- }
7315
+ process.stdout.write(chalk.dim(` ${fileName} ... `));
7316
+ await downloadFile(url, dest);
7317
+ const stat = fs.statSync(dest);
7318
+ console.log(chalk.green(`OK (${(stat.size / 1024).toFixed(0)} KB)`));
7590
7319
  ok++;
7591
7320
  } catch (e) {
7592
- console.log(` ${chalk.red('FAIL')} ${fileName}: ${e.message}`);
7321
+ console.log(chalk.red(`FAILED: ${e.message}`));
7593
7322
  fail++;
7594
7323
  }
7595
7324
  }
7596
-
7597
- console.log(chalk.bold(`\n Downloaded: ${ok}, Cached: ${cached}, Failed: ${fail}${verified ? `, Verified: ${verified}` : ''}\n`));
7598
- });
7599
-
7600
- // RVF cache management
7601
- rvfCmd.command('cache <action>')
7602
- .description('Manage local .rvf example cache (status, clear)')
7603
- .action((action) => {
7604
- const cacheDir = getRvfCacheDir();
7605
-
7606
- switch (action) {
7607
- case 'status': {
7608
- if (!fs.existsSync(cacheDir)) {
7609
- console.log(chalk.dim('\n No cache directory found.\n'));
7610
- return;
7611
- }
7612
- const files = fs.readdirSync(cacheDir).filter(f => f.endsWith('.rvf'));
7613
- const manifestExists = fs.existsSync(path.join(cacheDir, 'manifest.json'));
7614
- let totalSize = 0;
7615
- for (const f of files) {
7616
- totalSize += fs.statSync(path.join(cacheDir, f)).size;
7617
- }
7618
- console.log(chalk.bold.cyan('\nRVF Cache Status\n'));
7619
- console.log(` ${chalk.bold('Location:')} ${cacheDir}`);
7620
- console.log(` ${chalk.bold('Files:')} ${files.length} .rvf files`);
7621
- console.log(` ${chalk.bold('Size:')} ${(totalSize / (1024 * 1024)).toFixed(1)} MB`);
7622
- console.log(` ${chalk.bold('Manifest:')} ${manifestExists ? chalk.green('cached') : chalk.dim('not cached')}`);
7623
- if (manifestExists) {
7624
- const stat = fs.statSync(path.join(cacheDir, 'manifest.json'));
7625
- const age = Date.now() - stat.mtimeMs;
7626
- const fresh = age < 3600000;
7627
- console.log(` ${chalk.bold('Age:')} ${Math.floor(age / 60000)} min ${fresh ? chalk.green('(fresh)') : chalk.yellow('(stale)')}`);
7628
- }
7629
- console.log();
7630
- break;
7631
- }
7632
- case 'clear': {
7633
- if (!fs.existsSync(cacheDir)) {
7634
- console.log(chalk.dim('\n No cache to clear.\n'));
7635
- return;
7636
- }
7637
- const files = fs.readdirSync(cacheDir);
7638
- let cleared = 0;
7639
- for (const f of files) {
7640
- fs.unlinkSync(path.join(cacheDir, f));
7641
- cleared++;
7642
- }
7643
- console.log(chalk.green(`\n Cleared ${cleared} cached files from ${cacheDir}\n`));
7644
- break;
7645
- }
7646
- default:
7647
- console.error(chalk.red(`Unknown cache action: ${action}. Use: status, clear`));
7648
- process.exit(1);
7649
- }
7325
+ console.log(chalk.bold(`\nDone: ${ok} downloaded, ${fail} failed\n`));
7650
7326
  });
7651
7327
 
7652
7328
  // MCP Server command
@@ -7654,15 +7330,8 @@ const mcpCmd = program.command('mcp').description('MCP (Model Context Protocol)
7654
7330
 
7655
7331
  mcpCmd.command('start')
7656
7332
  .description('Start the RuVector MCP server')
7657
- .option('-t, --transport <type>', 'Transport type: stdio or sse', 'stdio')
7658
- .option('-p, --port <number>', 'Port for SSE transport', '8080')
7659
- .option('--host <host>', 'Host to bind for SSE', '0.0.0.0')
7660
- .action((opts) => {
7661
- if (opts.transport === 'sse') {
7662
- process.env.MCP_TRANSPORT = 'sse';
7663
- process.env.MCP_PORT = opts.port;
7664
- process.env.MCP_HOST = opts.host;
7665
- }
7333
+ .action(() => {
7334
+ // Execute the mcp-server.js directly
7666
7335
  const mcpServerPath = path.join(__dirname, 'mcp-server.js');
7667
7336
  if (!fs.existsSync(mcpServerPath)) {
7668
7337
  console.error(chalk.red('Error: MCP server not found at'), mcpServerPath);
@@ -7707,6 +7376,29 @@ mcpCmd.command('info')
7707
7376
  console.log(chalk.dim(' rvlite_cypher - Execute Cypher graph query'));
7708
7377
  console.log(chalk.dim(' rvlite_sparql - Execute SPARQL RDF query'));
7709
7378
 
7379
+ console.log(chalk.bold('\nBrain Tools (Shared Intelligence):'));
7380
+ console.log(chalk.dim(' brain_search - Semantic search shared knowledge'));
7381
+ console.log(chalk.dim(' brain_share - Share knowledge with brain'));
7382
+ console.log(chalk.dim(' brain_get - Get memory by ID'));
7383
+ console.log(chalk.dim(' brain_vote - Vote on quality'));
7384
+ console.log(chalk.dim(' brain_list - List memories'));
7385
+ console.log(chalk.dim(' brain_delete - Delete own contribution'));
7386
+ console.log(chalk.dim(' brain_status - System health'));
7387
+ console.log(chalk.dim(' brain_drift - Check knowledge drift'));
7388
+ console.log(chalk.dim(' brain_partition - MinCut knowledge topology'));
7389
+ console.log(chalk.dim(' brain_transfer - Domain transfer'));
7390
+ console.log(chalk.dim(' brain_sync - LoRA weight sync'));
7391
+
7392
+ console.log(chalk.bold('\nEdge Tools (Distributed Compute):'));
7393
+ console.log(chalk.dim(' edge_status - Network status'));
7394
+ console.log(chalk.dim(' edge_join - Join as compute node'));
7395
+ console.log(chalk.dim(' edge_balance - Check rUv balance'));
7396
+ console.log(chalk.dim(' edge_tasks - Available compute tasks'));
7397
+
7398
+ console.log(chalk.bold('\nIdentity Tools:'));
7399
+ console.log(chalk.dim(' identity_generate - Generate new pi key'));
7400
+ console.log(chalk.dim(' identity_show - Show current identity'));
7401
+
7710
7402
  console.log(chalk.bold('\n📦 Resources:'));
7711
7403
  console.log(chalk.dim(' ruvector://intelligence/stats - Current statistics'));
7712
7404
  console.log(chalk.dim(' ruvector://intelligence/patterns - Learned patterns'));
@@ -7728,166 +7420,163 @@ mcpCmd.command('info')
7728
7420
  console.log();
7729
7421
  });
7730
7422
 
7731
- // ============================================================================
7732
- // MCP tools subcommand
7733
- // ============================================================================
7734
-
7735
7423
  mcpCmd.command('tools')
7736
- .description('List all MCP tools organized by group')
7737
- .option('--group <name>', 'Filter by group (hooks, workers, rvf, rvlite, brain, edge, identity)')
7738
- .option('--json', 'Output as JSON')
7424
+ .description('List all MCP tools with descriptions (JSON output)')
7425
+ .option('--group <group>', 'Filter by group (hooks, workers, rvf, rvlite, brain, edge, identity)')
7426
+ .option('--json', 'JSON output')
7739
7427
  .action((opts) => {
7740
- const toolGroups = {
7428
+ const tools = {
7741
7429
  'hooks-core': [
7742
- { name: 'hooks_stats', args: '(none)', desc: 'Get intelligence statistics' },
7743
- { name: 'hooks_route', args: 'task, file?', desc: 'Route task to best agent' },
7744
- { name: 'hooks_remember', args: 'content, type?', desc: 'Store context in vector memory' },
7745
- { name: 'hooks_recall', args: 'query, limit?', desc: 'Search vector memory' },
7746
- { name: 'hooks_init', args: 'project_path?, force?', desc: 'Initialize hooks in project' },
7747
- { name: 'hooks_pretrain', args: 'scan_path?, patterns?', desc: 'Pretrain from repository' },
7748
- { name: 'hooks_build_agents', args: 'project_path?', desc: 'Generate agent configs' },
7749
- { name: 'hooks_verify', args: '(none)', desc: 'Verify hooks configuration' },
7750
- { name: 'hooks_doctor', args: 'fix?', desc: 'Diagnose setup issues' },
7751
- { name: 'hooks_export', args: 'format?', desc: 'Export intelligence data' },
7430
+ { name: 'hooks_stats', desc: 'Intelligence statistics' },
7431
+ { name: 'hooks_route', desc: 'Route task to best agent' },
7432
+ { name: 'hooks_remember', desc: 'Store in vector memory' },
7433
+ { name: 'hooks_recall', desc: 'Search vector memory' },
7434
+ { name: 'hooks_init', desc: 'Initialize hooks in project' },
7435
+ { name: 'hooks_pretrain', desc: 'Pretrain from repository' },
7436
+ { name: 'hooks_build_agents', desc: 'Generate agent configs' },
7437
+ { name: 'hooks_verify', desc: 'Verify hooks config' },
7438
+ { name: 'hooks_doctor', desc: 'Diagnose setup issues' },
7439
+ { name: 'hooks_export', desc: 'Export intelligence data' },
7440
+ { name: 'hooks_capabilities', desc: 'Get engine capabilities' },
7441
+ { name: 'hooks_import', desc: 'Import intelligence data' },
7442
+ { name: 'hooks_swarm_recommend', desc: 'Recommend agent for task' },
7443
+ { name: 'hooks_suggest_context', desc: 'Suggest relevant context' },
7752
7444
  ],
7753
7445
  'hooks-trajectory': [
7754
- { name: 'hooks_trajectory_start', args: 'task, context?', desc: 'Start learning trajectory' },
7755
- { name: 'hooks_trajectory_step', args: 'trajectory_id, action, result', desc: 'Record trajectory step' },
7756
- { name: 'hooks_trajectory_end', args: 'trajectory_id, outcome, score?', desc: 'End trajectory with outcome' },
7446
+ { name: 'hooks_trajectory_begin', desc: 'Begin execution trajectory' },
7447
+ { name: 'hooks_trajectory_step', desc: 'Add trajectory step' },
7448
+ { name: 'hooks_trajectory_end', desc: 'End trajectory with score' },
7757
7449
  ],
7758
7450
  'hooks-coedit': [
7759
- { name: 'hooks_pre_edit', args: 'file, changes', desc: 'Pre-edit analysis' },
7760
- { name: 'hooks_post_edit', args: 'file, changes, result', desc: 'Post-edit learning' },
7761
- { name: 'hooks_pre_command', args: 'command, args?', desc: 'Pre-command analysis' },
7762
- { name: 'hooks_post_command', args: 'command, exit_code, output?', desc: 'Post-command learning' },
7763
- { name: 'hooks_pre_task', args: 'task, context?', desc: 'Pre-task routing' },
7764
- { name: 'hooks_post_task', args: 'task, result, duration?', desc: 'Post-task learning' },
7451
+ { name: 'hooks_coedit_record', desc: 'Record co-edit pattern' },
7452
+ { name: 'hooks_coedit_suggest', desc: 'Suggest related files' },
7765
7453
  ],
7766
7454
  'hooks-errors': [
7767
- { name: 'hooks_error_learn', args: 'error, context?', desc: 'Learn from errors' },
7768
- { name: 'hooks_error_patterns', args: 'limit?', desc: 'Get learned error patterns' },
7769
- { name: 'hooks_error_suggest', args: 'error', desc: 'Suggest fix for error' },
7455
+ { name: 'hooks_error_record', desc: 'Record error and fix' },
7456
+ { name: 'hooks_error_suggest', desc: 'Suggest fixes for error' },
7457
+ { name: 'hooks_force_learn', desc: 'Force learning cycle' },
7770
7458
  ],
7771
7459
  'hooks-analysis': [
7772
- { name: 'hooks_complexity', args: 'file', desc: 'Analyze code complexity' },
7773
- { name: 'hooks_dependencies', args: 'file', desc: 'Analyze dependencies' },
7774
- { name: 'hooks_security_scan', args: 'file', desc: 'Security vulnerability scan' },
7775
- { name: 'hooks_test_coverage', args: 'file', desc: 'Estimate test coverage' },
7776
- { name: 'hooks_dead_code', args: 'file', desc: 'Detect dead code' },
7777
- { name: 'hooks_duplicate_code', args: 'file', desc: 'Find duplicate code' },
7460
+ { name: 'hooks_ast_analyze', desc: 'Parse AST, extract symbols' },
7461
+ { name: 'hooks_ast_complexity', desc: 'Cyclomatic complexity' },
7462
+ { name: 'hooks_diff_analyze', desc: 'Semantic diff analysis' },
7463
+ { name: 'hooks_diff_classify', desc: 'Classify change type' },
7464
+ { name: 'hooks_diff_similar', desc: 'Find similar past commits' },
7465
+ { name: 'hooks_coverage_route', desc: 'Coverage-aware routing' },
7466
+ { name: 'hooks_coverage_suggest', desc: 'Suggest tests for files' },
7467
+ { name: 'hooks_graph_mincut', desc: 'MinCut code boundaries' },
7468
+ { name: 'hooks_graph_cluster', desc: 'Code community detection' },
7469
+ { name: 'hooks_security_scan', desc: 'Security vulnerability scan' },
7470
+ { name: 'hooks_rag_context', desc: 'RAG-enhanced context' },
7471
+ { name: 'hooks_git_churn', desc: 'Git churn hot spots' },
7472
+ { name: 'hooks_route_enhanced', desc: 'Enhanced routing w/ signals' },
7473
+ { name: 'hooks_attention_info', desc: 'Attention mechanisms info' },
7474
+ { name: 'hooks_gnn_info', desc: 'GNN capabilities info' },
7778
7475
  ],
7779
7476
  'hooks-learning': [
7780
- { name: 'hooks_pattern_store', args: 'pattern, category, confidence?', desc: 'Store a learned pattern' },
7781
- { name: 'hooks_pattern_search', args: 'query, category?, limit?', desc: 'Search patterns' },
7782
- { name: 'hooks_attention', args: 'query, context', desc: 'Attention-weighted relevance' },
7477
+ { name: 'hooks_learning_config', desc: 'Configure learning algorithms (9 algos)' },
7478
+ { name: 'hooks_learning_stats', desc: 'Learning performance metrics' },
7479
+ { name: 'hooks_learning_update', desc: 'Record learning experience' },
7480
+ { name: 'hooks_learn', desc: 'Combined learn + recommend' },
7481
+ { name: 'hooks_algorithms_list', desc: 'List all algorithms' },
7482
+ { name: 'hooks_batch_learn', desc: 'Batch learning experiences' },
7783
7483
  ],
7784
7484
  'hooks-compress': [
7785
- { name: 'hooks_compress_context', args: 'content, max_tokens?', desc: 'Compress context' },
7786
- { name: 'hooks_compress_code', args: 'code, language?', desc: 'Compress code representation' },
7787
- { name: 'hooks_compress_diff', args: 'diff', desc: 'Compress diff' },
7485
+ { name: 'hooks_compress', desc: 'Compress pattern storage (10x savings)' },
7486
+ { name: 'hooks_compress_stats', desc: 'Compression statistics' },
7487
+ { name: 'hooks_compress_store', desc: 'Store compressed embedding' },
7488
+ { name: 'hooks_compress_get', desc: 'Retrieve compressed embedding' },
7788
7489
  ],
7789
7490
  'hooks-events': [
7790
- { name: 'hooks_session_start', args: '(none)', desc: 'Signal session start' },
7791
- { name: 'hooks_session_end', args: 'summary?', desc: 'Signal session end' },
7792
- { name: 'hooks_notify', args: 'message, level?', desc: 'Send notification' },
7793
- { name: 'hooks_transfer', args: 'target, data', desc: 'Transfer context' },
7794
- ],
7795
- 'hooks-model': [
7796
- { name: 'hooks_model_route', args: 'task, complexity?', desc: 'Route to optimal model tier' },
7797
- { name: 'hooks_model_outcome', args: 'model, task, success, tokens?', desc: 'Record model outcome' },
7798
- { name: 'hooks_model_stats', args: '(none)', desc: 'Get model routing stats' },
7491
+ { name: 'hooks_subscribe_snapshot', desc: 'Event state snapshot' },
7492
+ { name: 'hooks_watch_status', desc: 'File watch status' },
7799
7493
  ],
7800
7494
  'workers': [
7801
- { name: 'workers_list', args: '(none)', desc: 'List available workers' },
7802
- { name: 'workers_status', args: 'worker_id?', desc: 'Get worker status' },
7803
- { name: 'workers_dispatch', args: 'worker, task, args?', desc: 'Dispatch task to worker' },
7804
- { name: 'workers_cancel', args: 'job_id', desc: 'Cancel running job' },
7805
- { name: 'workers_detect', args: 'file', desc: 'Auto-detect applicable workers' },
7806
- { name: 'workers_complexity', args: 'file', desc: 'Worker: complexity analysis' },
7807
- { name: 'workers_dependencies', args: 'file', desc: 'Worker: dependency analysis' },
7808
- { name: 'workers_security', args: 'file', desc: 'Worker: security scan' },
7809
- { name: 'workers_coverage', args: 'file', desc: 'Worker: test coverage' },
7810
- { name: 'workers_dead_code', args: 'file', desc: 'Worker: dead code detection' },
7811
- { name: 'workers_duplicates', args: 'file', desc: 'Worker: duplicate detection' },
7812
- { name: 'workers_performance', args: 'file', desc: 'Worker: performance analysis' },
7495
+ { name: 'workers_dispatch', desc: 'Dispatch background worker' },
7496
+ { name: 'workers_status', desc: 'Worker status' },
7497
+ { name: 'workers_results', desc: 'Worker results' },
7498
+ { name: 'workers_triggers', desc: 'List worker triggers' },
7499
+ { name: 'workers_stats', desc: 'Worker statistics' },
7500
+ { name: 'workers_presets', desc: 'Available presets' },
7501
+ { name: 'workers_phases', desc: 'Worker phases' },
7502
+ { name: 'workers_create', desc: 'Create custom worker' },
7503
+ { name: 'workers_run', desc: 'Run worker' },
7504
+ { name: 'workers_custom', desc: 'Custom worker' },
7505
+ { name: 'workers_init_config', desc: 'Init config file' },
7506
+ { name: 'workers_load_config', desc: 'Load config' },
7813
7507
  ],
7814
7508
  'rvf': [
7815
- { name: 'rvf_create', args: 'path, dimension?, metric?', desc: 'Create new .rvf vector store' },
7816
- { name: 'rvf_open', args: 'path', desc: 'Open existing .rvf store' },
7817
- { name: 'rvf_ingest', args: 'path, vectors, ids?, metadata?', desc: 'Insert vectors' },
7818
- { name: 'rvf_query', args: 'path, vector, k?, filter?', desc: 'Query nearest neighbors' },
7819
- { name: 'rvf_delete', args: 'path, ids', desc: 'Delete vectors by ID' },
7820
- { name: 'rvf_status', args: 'path', desc: 'Get store status' },
7821
- { name: 'rvf_compact', args: 'path', desc: 'Compact store' },
7822
- { name: 'rvf_derive', args: 'parent_path, child_path', desc: 'COW-branch to child store' },
7823
- { name: 'rvf_segments', args: 'path', desc: 'List file segments' },
7824
- { name: 'rvf_examples', args: '(none)', desc: 'List example .rvf files' },
7509
+ { name: 'rvf_create', desc: 'Create .rvf vector store' },
7510
+ { name: 'rvf_open', desc: 'Open existing store' },
7511
+ { name: 'rvf_ingest', desc: 'Insert vectors' },
7512
+ { name: 'rvf_query', desc: 'Query nearest neighbors' },
7513
+ { name: 'rvf_delete', desc: 'Delete vectors by ID' },
7514
+ { name: 'rvf_status', desc: 'Store status' },
7515
+ { name: 'rvf_compact', desc: 'Compact store' },
7516
+ { name: 'rvf_derive', desc: 'COW-branch child store' },
7517
+ { name: 'rvf_segments', desc: 'List file segments' },
7518
+ { name: 'rvf_examples', desc: 'Example .rvf files' },
7825
7519
  ],
7826
7520
  'rvlite': [
7827
- { name: 'rvlite_sql', args: 'query, db_path?', desc: 'Execute SQL query' },
7828
- { name: 'rvlite_cypher', args: 'query, db_path?', desc: 'Execute Cypher graph query' },
7829
- { name: 'rvlite_sparql', args: 'query, db_path?', desc: 'Execute SPARQL RDF query' },
7521
+ { name: 'rvlite_sql', desc: 'SQL query over vector DB' },
7522
+ { name: 'rvlite_cypher', desc: 'Cypher graph query' },
7523
+ { name: 'rvlite_sparql', desc: 'SPARQL RDF query' },
7830
7524
  ],
7831
7525
  'brain': [
7832
- { name: 'brain_search', args: 'query, category?, limit?', desc: 'Semantic search shared brain' },
7833
- { name: 'brain_share', args: 'title, content, category, tags?, code_snippet?', desc: 'Share knowledge' },
7834
- { name: 'brain_get', args: 'id', desc: 'Retrieve memory by ID' },
7835
- { name: 'brain_vote', args: 'id, direction', desc: 'Quality vote (up/down)' },
7836
- { name: 'brain_list', args: 'category?, limit?', desc: 'List recent memories' },
7837
- { name: 'brain_delete', args: 'id', desc: 'Delete own contribution' },
7838
- { name: 'brain_status', args: '(none)', desc: 'System health' },
7839
- { name: 'brain_drift', args: 'domain?', desc: 'Check knowledge drift' },
7840
- { name: 'brain_partition', args: 'domain?, min_cluster_size?', desc: 'Knowledge topology' },
7841
- { name: 'brain_transfer', args: 'source_domain, target_domain', desc: 'Cross-domain transfer' },
7842
- { name: 'brain_sync', args: 'direction?', desc: 'LoRA weight sync' },
7526
+ { name: 'brain_search', desc: 'Semantic search shared knowledge' },
7527
+ { name: 'brain_share', desc: 'Share knowledge' },
7528
+ { name: 'brain_get', desc: 'Get memory by ID' },
7529
+ { name: 'brain_vote', desc: 'Vote on quality' },
7530
+ { name: 'brain_list', desc: 'List memories' },
7531
+ { name: 'brain_delete', desc: 'Delete own contribution' },
7532
+ { name: 'brain_status', desc: 'System health' },
7533
+ { name: 'brain_drift', desc: 'Knowledge drift check' },
7534
+ { name: 'brain_partition', desc: 'MinCut knowledge topology' },
7535
+ { name: 'brain_transfer', desc: 'Domain transfer' },
7536
+ { name: 'brain_sync', desc: 'LoRA weight sync' },
7843
7537
  ],
7844
7538
  'edge': [
7845
- { name: 'edge_status', args: '(none)', desc: 'Network status' },
7846
- { name: 'edge_join', args: 'contribution?', desc: 'Join compute network' },
7847
- { name: 'edge_balance', args: '(none)', desc: 'Check rUv balance' },
7848
- { name: 'edge_tasks', args: 'limit?', desc: 'List compute tasks' },
7539
+ { name: 'edge_status', desc: 'Network status' },
7540
+ { name: 'edge_join', desc: 'Join as compute node' },
7541
+ { name: 'edge_balance', desc: 'Check rUv balance' },
7542
+ { name: 'edge_tasks', desc: 'Available compute tasks' },
7849
7543
  ],
7850
7544
  'identity': [
7851
- { name: 'identity_generate', args: '(none)', desc: 'Generate new pi key' },
7852
- { name: 'identity_show', args: '(none)', desc: 'Show current identity' },
7545
+ { name: 'identity_generate', desc: 'Generate new pi key' },
7546
+ { name: 'identity_show', desc: 'Show current identity' },
7853
7547
  ],
7854
7548
  };
7855
7549
 
7856
- if (opts.json) {
7857
- const output = {};
7858
- Object.entries(toolGroups).forEach(([group, tools]) => {
7859
- if (!opts.group || group === opts.group || group.startsWith(opts.group)) {
7860
- output[group] = tools;
7861
- }
7862
- });
7863
- console.log(JSON.stringify(output, null, 2));
7550
+ // Filter by group if specified
7551
+ let groups = Object.entries(tools);
7552
+ if (opts.group) {
7553
+ groups = groups.filter(([g]) => g === opts.group || g.startsWith(opts.group));
7554
+ }
7555
+
7556
+ if (opts.json || !process.stdout.isTTY) {
7557
+ const flat = groups.flatMap(([group, items]) => items.map(t => ({ ...t, group })));
7558
+ console.log(JSON.stringify(flat, null, 2));
7864
7559
  return;
7865
7560
  }
7866
7561
 
7867
- console.log(chalk.bold.cyan('\nRuVector MCP Tools\n'));
7868
7562
  let total = 0;
7869
- Object.entries(toolGroups).forEach(([group, tools]) => {
7870
- if (opts.group && group !== opts.group && !group.startsWith(opts.group)) return;
7871
- console.log(chalk.bold.yellow(` ${group} (${tools.length}):`));
7872
- tools.forEach(t => {
7873
- console.log(` ${chalk.green(t.name.padEnd(28))} ${chalk.dim(t.args.padEnd(40))} ${t.desc}`);
7563
+ groups.forEach(([group, items]) => {
7564
+ console.log(chalk.bold.cyan(`\n${group} (${items.length} tools)`));
7565
+ items.forEach(t => {
7566
+ console.log(` ${chalk.bold(t.name.padEnd(28))} ${chalk.dim(t.desc)}`);
7874
7567
  });
7875
- console.log();
7876
- total += tools.length;
7568
+ total += items.length;
7877
7569
  });
7878
- console.log(chalk.bold(`Total: ${total} MCP tools\n`));
7570
+ console.log(chalk.bold(`\nTotal: ${total} MCP tools\n`));
7879
7571
  });
7880
7572
 
7881
- // ============================================================================
7882
- // MCP test subcommand
7883
- // ============================================================================
7884
-
7885
7573
  mcpCmd.command('test')
7886
7574
  .description('Test MCP server setup and tool registration')
7887
7575
  .action(() => {
7888
7576
  console.log(chalk.bold.cyan('\nMCP Server Test Results'));
7889
7577
  console.log(chalk.dim('-'.repeat(40)));
7890
7578
 
7579
+ // Test 1: server file exists
7891
7580
  const mcpServerPath = path.join(__dirname, 'mcp-server.js');
7892
7581
  if (fs.existsSync(mcpServerPath)) {
7893
7582
  console.log(` ${chalk.green('PASS')} mcp-server.js exists`);
@@ -7896,6 +7585,7 @@ mcpCmd.command('test')
7896
7585
  process.exit(1);
7897
7586
  }
7898
7587
 
7588
+ // Test 2: syntax check
7899
7589
  try {
7900
7590
  const { execSync } = require('child_process');
7901
7591
  execSync(`node -c ${mcpServerPath}`, { stdio: 'pipe' });
@@ -7905,6 +7595,7 @@ mcpCmd.command('test')
7905
7595
  process.exit(1);
7906
7596
  }
7907
7597
 
7598
+ // Test 3: MCP SDK available
7908
7599
  try {
7909
7600
  require('@modelcontextprotocol/sdk/server/index.js');
7910
7601
  console.log(` ${chalk.green('PASS')} @modelcontextprotocol/sdk installed`);
@@ -7913,10 +7604,13 @@ mcpCmd.command('test')
7913
7604
  process.exit(1);
7914
7605
  }
7915
7606
 
7607
+ // Test 4: count tools by parsing TOOLS array entries (each has inputSchema)
7916
7608
  try {
7917
7609
  const src = fs.readFileSync(mcpServerPath, 'utf8');
7610
+ // Extract the TOOLS array section (from 'const TOOLS = [' to the matching '];')
7918
7611
  const toolsStart = src.indexOf('const TOOLS = [');
7919
7612
  const toolsSection = toolsStart >= 0 ? src.slice(toolsStart) : src;
7613
+ // Match tool names that are followed by inputSchema (real MCP tools only)
7920
7614
  const toolDefs = toolsSection.match(/name:\s*'([a-z][a-z0-9_]*)'\s*,\s*\n\s*description:/g) || [];
7921
7615
  const toolNames = toolDefs.map(m => m.match(/name:\s*'([a-z][a-z0-9_]*)'/)[1]);
7922
7616
  const groups = {};
@@ -7933,6 +7627,7 @@ mcpCmd.command('test')
7933
7627
  console.log(` ${chalk.yellow('WARN')} Could not parse tool count: ${e.message}`);
7934
7628
  }
7935
7629
 
7630
+ // Test 5: version check
7936
7631
  try {
7937
7632
  const src = fs.readFileSync(mcpServerPath, 'utf8');
7938
7633
  const verMatch = src.match(/version:\s*'([^']+)'/);
@@ -7948,1103 +7643,1424 @@ mcpCmd.command('test')
7948
7643
  });
7949
7644
 
7950
7645
  // ============================================================================
7951
- // Brain Commands — Shared intelligence via pi.ruv.io REST API (direct fetch)
7646
+ // Brain Commands — Shared intelligence via @ruvector/pi-brain (lazy-loaded)
7952
7647
  // ============================================================================
7953
7648
 
7954
- function getBrainConfig(opts) {
7955
- return {
7956
- url: opts.url || process.env.BRAIN_URL || 'https://pi.ruv.io',
7957
- key: opts.key || process.env.PI
7958
- };
7649
+ // Lazy load pi-brain (optional peer dependency)
7650
+ async function requirePiBrain() {
7651
+ try {
7652
+ return await import('@ruvector/pi-brain');
7653
+ } catch {
7654
+ console.error(chalk.red('Brain commands require @ruvector/pi-brain'));
7655
+ console.error(chalk.yellow(' npm install @ruvector/pi-brain'));
7656
+ process.exit(1);
7657
+ }
7959
7658
  }
7960
7659
 
7961
- function brainHeaders(config) {
7962
- const h = { 'Content-Type': 'application/json' };
7963
- if (config.key) h['Authorization'] = `Bearer ${config.key}`;
7964
- return h;
7660
+ // Determine output mode: JSON when --json flag or piped
7661
+ function isJsonOutput(opts) {
7662
+ return opts.json || !process.stdout.isTTY;
7965
7663
  }
7966
7664
 
7967
- async function brainFetch(config, endpoint, opts = {}) {
7968
- const url = new URL(endpoint, config.url);
7969
- if (opts.params) {
7970
- for (const [k, v] of Object.entries(opts.params)) {
7971
- if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
7972
- }
7973
- }
7974
- const fetchOpts = { headers: brainHeaders(config), signal: AbortSignal.timeout(30000) };
7975
- if (opts.method) fetchOpts.method = opts.method;
7976
- if (opts.body) { fetchOpts.method = opts.method || 'POST'; fetchOpts.body = JSON.stringify(opts.body); }
7977
- const resp = await proxyFetch(url.toString(), fetchOpts);
7978
- if (!resp.ok) {
7979
- const errText = await resp.text().catch(() => resp.statusText);
7980
- throw new Error(`${resp.status} ${errText}`);
7981
- }
7982
- if (resp.status === 204 || resp.headers.get('content-length') === '0') return {};
7983
- return resp.json();
7665
+ // Create a PiBrainClient from command options
7666
+ async function makeBrainClient(opts) {
7667
+ const { PiBrainClient } = await requirePiBrain();
7668
+ return new PiBrainClient({
7669
+ url: opts.url || process.env.BRAIN_URL || 'https://pi.ruv.io',
7670
+ apiKey: process.env.PI || 'anonymous',
7671
+ });
7984
7672
  }
7985
7673
 
7986
- const brainCmd = program.command('brain').description('Shared intelligence — search, share, and manage collective knowledge');
7674
+ const brainCmd = program
7675
+ .command('brain')
7676
+ .description('Shared intelligence — search, share, and manage collective knowledge')
7677
+ .option('--url <url>', 'Brain server URL', process.env.BRAIN_URL || 'https://pi.ruv.io')
7678
+ .option('--json', 'Force JSON output');
7987
7679
 
7988
- brainCmd.command('search <query>')
7989
- .description('Semantic search across shared brain knowledge')
7990
- .option('-c, --category <cat>', 'Filter by category')
7680
+ brainCmd
7681
+ .command('search <query>')
7682
+ .description('Semantic search across collective knowledge')
7991
7683
  .option('-l, --limit <n>', 'Max results', '10')
7992
- .option('--url <url>', 'Brain server URL')
7993
- .option('--key <key>', 'Pi key')
7994
- .option('--json', 'Output as JSON')
7995
- .option('--verbose', 'Show detailed scoring and metadata per result')
7996
- .action(async (query, opts) => {
7997
- const config = getBrainConfig(opts);
7998
- try {
7999
- const results = await brainFetch(config, '/v1/memories/search', { params: { q: query, category: opts.category, limit: opts.limit } });
8000
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(results, null, 2)); return; }
8001
- console.log(chalk.bold.cyan(`\nBrain Search: "${query}"\n`));
8002
- if (!results.length) { console.log(chalk.dim(' No results found.\n')); return; }
8003
- results.forEach((r, i) => {
8004
- console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(r.title || r.id)}`);
8005
- if (r.category) console.log(` ${chalk.dim('Category:')} ${r.category}`);
8006
- if (r.score) console.log(` ${chalk.dim('Score:')} ${r.score.toFixed(3)}`);
8007
- if (opts.verbose) {
8008
- if (r.quality_score !== undefined) console.log(` ${chalk.dim('Quality:')} ${typeof r.quality_score === 'number' ? r.quality_score.toFixed(3) : r.quality_score}`);
8009
- if (r.votes_up !== undefined || r.votes_down !== undefined) console.log(` ${chalk.dim('Votes:')} ${r.votes_up || 0}↑ ${r.votes_down || 0}↓`);
8010
- if (r.witness_hash) console.log(` ${chalk.dim('Witness:')} ${r.witness_hash.slice(0, 12)}...`);
8011
- if (r.contributor_id) console.log(` ${chalk.dim('Contributor:')} ${r.contributor_id}`);
8012
- if (r.created_at) console.log(` ${chalk.dim('Created:')} ${r.created_at}`);
8013
- if (r.tags && r.tags.length) console.log(` ${chalk.dim('Tags:')} ${r.tags.join(', ')}`);
8014
- }
8015
- console.log();
7684
+ .option('-c, --category <category>', 'Filter by category')
7685
+ .action(async (query, cmdOpts) => {
7686
+ const opts = brainCmd.opts();
7687
+ const spinner = ora('Searching brain...').start();
7688
+ try {
7689
+ const client = await makeBrainClient(opts);
7690
+ const result = await client.search({
7691
+ query,
7692
+ category: cmdOpts.category || undefined,
7693
+ limit: parseInt(cmdOpts.limit),
8016
7694
  });
8017
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7695
+ const items = Array.isArray(result) ? result : (result.memories || result.results || []);
7696
+ spinner.succeed(chalk.green(`Found ${items.length} result(s)`));
7697
+ if (isJsonOutput(opts)) {
7698
+ console.log(JSON.stringify(result, null, 2));
7699
+ } else {
7700
+ items.forEach((item, i) => {
7701
+ console.log(chalk.cyan(`\n ${i + 1}. ${item.title || item.id || 'Untitled'}`));
7702
+ if (item.category) console.log(chalk.gray(` Category: ${item.category}`));
7703
+ if (item.quality_score != null) console.log(chalk.gray(` Quality: ${item.quality_score}`));
7704
+ if (item.content) console.log(chalk.dim(` ${item.content.substring(0, 120)}${item.content.length > 120 ? '...' : ''}`));
7705
+ });
7706
+ }
7707
+ } catch (error) {
7708
+ spinner.fail(chalk.red('Search failed'));
7709
+ console.error(chalk.red(error.message));
7710
+ process.exit(1);
7711
+ }
8018
7712
  });
8019
7713
 
8020
- brainCmd.command('share <title>')
7714
+ brainCmd
7715
+ .command('share <title>')
8021
7716
  .description('Share knowledge with the collective brain')
8022
- .requiredOption('-c, --category <cat>', 'Category (pattern, solution, architecture, convention, security, performance, tooling)')
7717
+ .requiredOption('-c, --category <category>', 'Category (architecture, pattern, solution, etc.)')
8023
7718
  .option('-t, --tags <tags>', 'Comma-separated tags')
8024
- .option('--content <text>', 'Content body')
8025
- .option('--code <snippet>', 'Code snippet')
8026
- .option('--url <url>', 'Brain server URL')
8027
- .option('--key <key>', 'Pi key')
8028
- .option('--json', 'Output as JSON')
8029
- .action(async (title, opts) => {
8030
- const config = getBrainConfig(opts);
8031
- try {
8032
- const result = await brainFetch(config, '/v1/memories', { body: { title, content: opts.content || title, category: opts.category, tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : [], code_snippet: opts.code } });
8033
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8034
- console.log(chalk.green(`Shared: ${result.id || 'OK'}`));
8035
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7719
+ .option('--content <content>', 'Content body (reads from stdin if omitted)')
7720
+ .action(async (title, cmdOpts) => {
7721
+ const opts = brainCmd.opts();
7722
+ const spinner = ora('Sharing knowledge...').start();
7723
+ try {
7724
+ const client = await makeBrainClient(opts);
7725
+ let content = cmdOpts.content;
7726
+ if (!content && !process.stdin.isTTY) {
7727
+ const chunks = [];
7728
+ for await (const chunk of process.stdin) chunks.push(chunk);
7729
+ content = Buffer.concat(chunks).toString('utf8').trim();
7730
+ }
7731
+ if (!content) {
7732
+ spinner.fail(chalk.red('No content provided. Use --content or pipe via stdin.'));
7733
+ process.exit(1);
7734
+ }
7735
+ const tags = cmdOpts.tags ? cmdOpts.tags.split(',').map(t => t.trim()) : [];
7736
+ const result = await client.share({ title, category: cmdOpts.category, content, tags });
7737
+ spinner.succeed(chalk.green('Knowledge shared'));
7738
+ if (isJsonOutput(opts)) {
7739
+ console.log(JSON.stringify(result, null, 2));
7740
+ } else {
7741
+ console.log(chalk.cyan(` ID: ${result.id || 'N/A'}`));
7742
+ console.log(chalk.gray(` Title: ${title}`));
7743
+ console.log(chalk.gray(` Category: ${cmdOpts.category}`));
7744
+ if (tags.length) console.log(chalk.gray(` Tags: ${tags.join(', ')}`));
7745
+ }
7746
+ } catch (error) {
7747
+ spinner.fail(chalk.red('Failed to share knowledge'));
7748
+ console.error(chalk.red(error.message));
7749
+ process.exit(1);
7750
+ }
8036
7751
  });
8037
7752
 
8038
- brainCmd.command('get <id>')
8039
- .description('Retrieve a specific memory by ID')
8040
- .option('--url <url>', 'Brain server URL')
8041
- .option('--key <key>', 'Pi key')
8042
- .option('--json', 'Output as JSON')
8043
- .action(async (id, opts) => {
8044
- const config = getBrainConfig(opts);
7753
+ brainCmd
7754
+ .command('get <id>')
7755
+ .description('Retrieve a memory by ID with full provenance')
7756
+ .action(async (id) => {
7757
+ const opts = brainCmd.opts();
7758
+ const spinner = ora('Fetching memory...').start();
8045
7759
  try {
8046
- const result = await brainFetch(config, `/v1/memories/${id}`);
8047
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8048
- console.log(chalk.bold.cyan(`\nMemory: ${id}\n`));
8049
- if (result.title) console.log(` ${chalk.bold('Title:')} ${result.title}`);
8050
- if (result.content) console.log(` ${chalk.bold('Content:')} ${result.content}`);
8051
- if (result.category) console.log(` ${chalk.bold('Category:')} ${result.category}`);
8052
- console.log();
8053
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8054
- });
8055
-
8056
- brainCmd.command('vote <id> <direction>')
8057
- .description('Quality vote on a memory (up or down)')
8058
- .option('--url <url>', 'Brain server URL')
8059
- .option('--key <key>', 'Pi key')
8060
- .option('--json', 'Output as JSON')
8061
- .action(async (id, direction, opts) => {
8062
- const config = getBrainConfig(opts);
7760
+ const client = await makeBrainClient(opts);
7761
+ const result = await client.get(id);
7762
+ spinner.succeed(chalk.green('Memory retrieved'));
7763
+ if (isJsonOutput(opts)) {
7764
+ console.log(JSON.stringify(result, null, 2));
7765
+ } else {
7766
+ console.log(chalk.cyan(` Title: ${result.title || 'N/A'}`));
7767
+ console.log(chalk.gray(` ID: ${result.id || id}`));
7768
+ console.log(chalk.gray(` Category: ${result.category || 'N/A'}`));
7769
+ console.log(chalk.gray(` Quality: ${result.quality_score != null ? result.quality_score : 'N/A'}`));
7770
+ console.log(chalk.gray(` Created: ${result.created_at || 'N/A'}`));
7771
+ if (result.tags && result.tags.length) console.log(chalk.gray(` Tags: ${result.tags.join(', ')}`));
7772
+ if (result.content) {
7773
+ console.log(chalk.white('\n Content:'));
7774
+ console.log(chalk.dim(` ${result.content}`));
7775
+ }
7776
+ }
7777
+ } catch (error) {
7778
+ spinner.fail(chalk.red('Failed to retrieve memory'));
7779
+ console.error(chalk.red(error.message));
7780
+ process.exit(1);
7781
+ }
7782
+ });
7783
+
7784
+ brainCmd
7785
+ .command('vote <id> <direction>')
7786
+ .description('Vote on a memory (up or down)')
7787
+ .action(async (id, direction) => {
7788
+ const opts = brainCmd.opts();
7789
+ if (!['up', 'down'].includes(direction)) {
7790
+ console.error(chalk.red('Direction must be "up" or "down"'));
7791
+ process.exit(1);
7792
+ }
7793
+ const spinner = ora(`Voting ${direction} on ${id}...`).start();
8063
7794
  try {
8064
- const result = await brainFetch(config, `/v1/memories/${id}/vote`, { body: { direction } });
8065
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8066
- console.log(chalk.green(`Voted ${direction} on ${id}`));
8067
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7795
+ const client = await makeBrainClient(opts);
7796
+ const result = await client.vote(id, direction);
7797
+ spinner.succeed(chalk.green(`Voted ${direction} on memory ${id}`));
7798
+ if (isJsonOutput(opts)) {
7799
+ console.log(JSON.stringify(result, null, 2));
7800
+ }
7801
+ } catch (error) {
7802
+ spinner.fail(chalk.red('Vote failed'));
7803
+ console.error(chalk.red(error.message));
7804
+ process.exit(1);
7805
+ }
8068
7806
  });
8069
7807
 
8070
- brainCmd.command('list')
8071
- .description('List recent shared memories')
8072
- .option('-c, --category <cat>', 'Filter by category')
7808
+ brainCmd
7809
+ .command('list')
7810
+ .description('List memories from the collective brain')
7811
+ .option('-c, --category <category>', 'Filter by category')
8073
7812
  .option('-l, --limit <n>', 'Max results', '20')
8074
- .option('--url <url>', 'Brain server URL')
8075
- .option('--key <key>', 'Pi key')
8076
- .option('--json', 'Output as JSON')
8077
- .option('--offset <n>', 'Skip first N results (pagination)', '0')
8078
- .option('--sort <field>', 'Sort by: updated_at, quality, votes', 'updated_at')
8079
- .option('--tags <tags>', 'Filter by tags (comma-separated)')
8080
- .action(async (opts) => {
8081
- const config = getBrainConfig(opts);
8082
- try {
8083
- const data = await brainFetch(config, '/v1/memories/list', { params: { category: opts.category, limit: opts.limit, offset: opts.offset, sort: opts.sort, tags: opts.tags } });
8084
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8085
- console.log(chalk.bold.cyan('\nShared Brain Memories\n'));
8086
- const memories = Array.isArray(data) ? data : data.memories || [];
8087
- if (data.total_count !== undefined) {
8088
- console.log(chalk.dim(` Showing ${memories.length} of ${data.total_count} (offset ${data.offset || 0})\n`));
8089
- }
8090
- if (!memories.length) { console.log(chalk.dim(' No memories found.\n')); return; }
8091
- memories.forEach((r, i) => {
8092
- console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(r.title || r.id)} ${chalk.dim(`[${r.category || 'unknown'}]`)}`);
8093
- });
8094
- console.log();
8095
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7813
+ .action(async (cmdOpts) => {
7814
+ const opts = brainCmd.opts();
7815
+ const spinner = ora('Listing memories...').start();
7816
+ try {
7817
+ const client = await makeBrainClient(opts);
7818
+ const result = await client.list(
7819
+ cmdOpts.category || undefined,
7820
+ parseInt(cmdOpts.limit),
7821
+ );
7822
+ const items = Array.isArray(result) ? result : (result.memories || result.results || []);
7823
+ spinner.succeed(chalk.green(`${items.length} memor${items.length === 1 ? 'y' : 'ies'} found`));
7824
+ if (isJsonOutput(opts)) {
7825
+ console.log(JSON.stringify(result, null, 2));
7826
+ } else {
7827
+ items.forEach((item, i) => {
7828
+ console.log(chalk.cyan(` ${i + 1}. ${item.title || item.id || 'Untitled'}`) +
7829
+ (item.category ? chalk.gray(` [${item.category}]`) : '') +
7830
+ (item.quality_score != null ? chalk.yellow(` (q: ${item.quality_score})`) : ''));
7831
+ });
7832
+ }
7833
+ } catch (error) {
7834
+ spinner.fail(chalk.red('Failed to list memories'));
7835
+ console.error(chalk.red(error.message));
7836
+ process.exit(1);
7837
+ }
8096
7838
  });
8097
7839
 
8098
- brainCmd.command('delete <id>')
8099
- .description('Delete your own contribution')
8100
- .option('--url <url>', 'Brain server URL')
8101
- .option('--key <key>', 'Pi key')
8102
- .option('--json', 'Output as JSON')
8103
- .action(async (id, opts) => {
8104
- const config = getBrainConfig(opts);
7840
+ brainCmd
7841
+ .command('delete <id>')
7842
+ .description('Delete a memory you contributed')
7843
+ .action(async (id) => {
7844
+ const opts = brainCmd.opts();
7845
+ const spinner = ora(`Deleting memory ${id}...`).start();
8105
7846
  try {
8106
- const result = await brainFetch(config, `/v1/memories/${id}`, { method: 'DELETE' });
8107
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify({ deleted: true, id }, null, 2)); return; }
8108
- console.log(chalk.green(`Deleted: ${id}`));
8109
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7847
+ const client = await makeBrainClient(opts);
7848
+ const result = await client.delete(id);
7849
+ spinner.succeed(chalk.green(`Memory ${id} deleted`));
7850
+ if (isJsonOutput(opts)) {
7851
+ console.log(JSON.stringify(result, null, 2));
7852
+ }
7853
+ } catch (error) {
7854
+ spinner.fail(chalk.red('Failed to delete memory'));
7855
+ console.error(chalk.red(error.message));
7856
+ process.exit(1);
7857
+ }
8110
7858
  });
8111
7859
 
8112
- brainCmd.command('status')
8113
- .description('Show shared brain system health')
8114
- .option('--url <url>', 'Brain server URL')
8115
- .option('--key <key>', 'Pi key')
8116
- .option('--json', 'Output as JSON')
8117
- .action(async (opts) => {
8118
- const config = getBrainConfig(opts);
8119
- try {
8120
- const status = await brainFetch(config, '/v1/status');
8121
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(status, null, 2)); return; }
8122
- console.log(chalk.bold.cyan('\nBrain Status\n'));
8123
- Object.entries(status).forEach(([k, v]) => {
8124
- console.log(` ${chalk.bold(k + ':')} ${v}`);
8125
- });
8126
- // AGI subsystem fields
8127
- if (status.sona_patterns !== undefined) {
8128
- console.log(chalk.bold('\n AGI Subsystems'));
8129
- if (status.sona_patterns !== undefined) console.log(` ${chalk.dim('SONA Patterns:')} ${status.sona_patterns} ${chalk.dim('Trajectories:')} ${status.sona_trajectories || 0}`);
8130
- if (status.gwt_workspace_load !== undefined) console.log(` ${chalk.dim('GWT Load:')} ${status.gwt_workspace_load} ${chalk.dim('Avg Salience:')} ${status.gwt_avg_salience || 0}`);
8131
- if (status.knowledge_velocity !== undefined) console.log(` ${chalk.dim('Temporal Velocity:')} ${status.knowledge_velocity}/hr ${chalk.dim('Deltas:')} ${status.temporal_deltas || 0}`);
8132
- if (status.meta_avg_regret !== undefined) console.log(` ${chalk.dim('Meta Regret:')} ${status.meta_avg_regret} ${chalk.dim('Plateau:')} ${status.meta_plateau_status || 'unknown'}`);
8133
- }
8134
- if (status.midstream_scheduler_ticks !== undefined) {
8135
- console.log(chalk.bold('\n Midstream'));
8136
- console.log(` ${chalk.dim('Scheduler Ticks:')} ${status.midstream_scheduler_ticks}`);
8137
- console.log(` ${chalk.dim('Attractor Categories:')} ${status.midstream_attractor_categories || 0}`);
8138
- console.log(` ${chalk.dim('Strange-Loop:')} v${status.midstream_strange_loop_version || '?'}`);
8139
- }
8140
- console.log();
8141
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8142
- });
8143
-
8144
- brainCmd.command('drift')
8145
- .description('Check if shared knowledge has drifted')
8146
- .option('-d, --domain <domain>', 'Domain to check')
8147
- .option('--url <url>', 'Brain server URL')
8148
- .option('--key <key>', 'Pi key')
8149
- .option('--json', 'Output as JSON')
8150
- .action(async (opts) => {
8151
- const config = getBrainConfig(opts);
8152
- try {
8153
- const report = await brainFetch(config, '/v1/drift', { params: { domain: opts.domain } });
8154
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(report, null, 2)); return; }
8155
- console.log(chalk.bold.cyan('\nDrift Report\n'));
8156
- console.log(` ${chalk.bold('Drifting:')} ${report.is_drifting ? chalk.red('Yes') : chalk.green('No')}`);
8157
- if (report.cv) console.log(` ${chalk.bold('CV:')} ${report.cv}`);
8158
- console.log();
8159
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8160
- });
8161
-
8162
- brainCmd.command('partition')
8163
- .description('Get knowledge partitioned by mincut topology')
8164
- .option('-d, --domain <domain>', 'Domain to partition')
8165
- .option('--min-size <n>', 'Minimum cluster size', '3')
8166
- .option('--url <url>', 'Brain server URL')
8167
- .option('--key <key>', 'Pi key')
8168
- .option('--json', 'Output as JSON')
8169
- .action(async (opts) => {
8170
- const config = getBrainConfig(opts);
7860
+ brainCmd
7861
+ .command('status')
7862
+ .description('Show brain system health and statistics')
7863
+ .action(async () => {
7864
+ const opts = brainCmd.opts();
7865
+ const spinner = ora('Fetching brain status...').start();
8171
7866
  try {
8172
- const result = await brainFetch(config, '/v1/partition', { params: { domain: opts.domain, min_cluster_size: opts.minSize } });
8173
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8174
- console.log(chalk.bold.cyan('\nKnowledge Partitions\n'));
8175
- if (result.clusters) {
8176
- result.clusters.forEach((c, i) => {
8177
- console.log(` ${chalk.yellow('Cluster ' + (i + 1) + ':')} ${c.size || 'unknown'} entries`);
8178
- });
7867
+ const client = await makeBrainClient(opts);
7868
+ const result = await client.status();
7869
+ spinner.succeed(chalk.green('Brain status'));
7870
+ if (isJsonOutput(opts)) {
7871
+ console.log(JSON.stringify(result, null, 2));
7872
+ } else {
7873
+ const s = result;
7874
+ console.log(chalk.cyan(` Memories: ${s.total_memories != null ? s.total_memories : 'N/A'}`));
7875
+ console.log(chalk.cyan(` Contributors: ${s.total_contributors != null ? s.total_contributors : 'N/A'}`));
7876
+ console.log(chalk.cyan(` Quality: ${s.avg_quality != null ? s.avg_quality : 'N/A'}`));
7877
+ console.log(chalk.cyan(` Drift: ${s.drift_status || s.drift || 'N/A'}`));
7878
+ if (s.uptime) console.log(chalk.gray(` Uptime: ${s.uptime}`));
8179
7879
  }
8180
- console.log();
8181
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7880
+ } catch (error) {
7881
+ spinner.fail(chalk.red('Failed to get brain status'));
7882
+ console.error(chalk.red(error.message));
7883
+ process.exit(1);
7884
+ }
8182
7885
  });
8183
7886
 
8184
- brainCmd.command('transfer <source> <target>')
8185
- .description('Apply learned priors from one domain to another')
8186
- .option('--url <url>', 'Brain server URL')
8187
- .option('--key <key>', 'Pi key')
8188
- .option('--json', 'Output as JSON')
8189
- .action(async (source, target, opts) => {
8190
- const config = getBrainConfig(opts);
7887
+ brainCmd
7888
+ .command('drift [domain]')
7889
+ .description('Check knowledge drift for a domain')
7890
+ .action(async (domain) => {
7891
+ const opts = brainCmd.opts();
7892
+ const spinner = ora('Checking drift...').start();
8191
7893
  try {
8192
- const result = await brainFetch(config, '/v1/transfer', { body: { source_domain: source, target_domain: target } });
8193
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8194
- console.log(chalk.green(`Transfer ${source} -> ${target}: ${result.status || 'OK'}`));
8195
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7894
+ const client = await makeBrainClient(opts);
7895
+ const result = await client.drift(domain || undefined);
7896
+ spinner.succeed(chalk.green('Drift analysis complete'));
7897
+ if (isJsonOutput(opts)) {
7898
+ console.log(JSON.stringify(result, null, 2));
7899
+ } else {
7900
+ if (domain) console.log(chalk.gray(` Domain: ${domain}`));
7901
+ console.log(chalk.gray(` Drift: ${result.drift_score != null ? result.drift_score : JSON.stringify(result)}`));
7902
+ if (result.status) console.log(chalk.gray(` Status: ${result.status}`));
7903
+ }
7904
+ } catch (error) {
7905
+ spinner.fail(chalk.red('Drift check failed'));
7906
+ console.error(chalk.red(error.message));
7907
+ process.exit(1);
7908
+ }
8196
7909
  });
8197
7910
 
8198
- brainCmd.command('train')
8199
- .description('Trigger a training cycle (SONA pattern learning + domain evolution)')
8200
- .option('--url <url>', 'Brain server URL')
8201
- .option('--key <key>', 'Pi key')
8202
- .option('--json', 'Output as JSON')
8203
- .action(async (opts) => {
8204
- const config = getBrainConfig(opts);
7911
+ brainCmd
7912
+ .command('partition [domain]')
7913
+ .description('View knowledge topology / partitioning')
7914
+ .action(async (domain) => {
7915
+ const opts = brainCmd.opts();
7916
+ const spinner = ora('Fetching partition info...').start();
8205
7917
  try {
8206
- const result = await brainFetch(config, '/v1/train', { method: 'POST', body: {} });
8207
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8208
- console.log(chalk.bold.cyan('\nTraining Cycle Complete\n'));
8209
- console.log(` ${chalk.bold('SONA:')} ${result.sona_message}`);
8210
- console.log(` ${chalk.bold('Patterns:')} ${result.sona_patterns}`);
8211
- console.log(` ${chalk.bold('Pareto:')} ${result.pareto_before} → ${result.pareto_after}`);
8212
- console.log(` ${chalk.bold('Memories:')} ${result.memory_count}`);
8213
- console.log(` ${chalk.bold('Votes:')} ${result.vote_count}`);
8214
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8215
- });
8216
-
8217
- brainCmd.command('sync [direction]')
8218
- .description('Synchronize LoRA weights (pull, push, or both)')
8219
- .option('--url <url>', 'Brain server URL')
8220
- .option('--key <key>', 'Pi key')
8221
- .option('--json', 'Output as JSON')
8222
- .action(async (direction, opts) => {
8223
- const config = getBrainConfig(opts);
8224
- try {
8225
- const result = await brainFetch(config, '/v1/lora/latest', { params: { direction: direction || 'both' } });
8226
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8227
- console.log(chalk.green(`Sync ${direction || 'both'}: ${result.status || 'OK'}`));
8228
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7918
+ const client = await makeBrainClient(opts);
7919
+ const result = await client.partition(domain || undefined);
7920
+ spinner.succeed(chalk.green('Partition data retrieved'));
7921
+ if (isJsonOutput(opts)) {
7922
+ console.log(JSON.stringify(result, null, 2));
7923
+ } else {
7924
+ if (domain) console.log(chalk.gray(` Domain: ${domain}`));
7925
+ console.log(chalk.dim(` ${JSON.stringify(result, null, 2)}`));
7926
+ }
7927
+ } catch (error) {
7928
+ spinner.fail(chalk.red('Partition query failed'));
7929
+ console.error(chalk.red(error.message));
7930
+ process.exit(1);
7931
+ }
8229
7932
  });
8230
7933
 
8231
- brainCmd.command('page <action> [args...]')
8232
- .description('Brainpedia page management (list, get, create, update, delete)')
8233
- .option('--url <url>', 'Brain server URL')
8234
- .option('--key <key>', 'Pi key')
8235
- .option('--json', 'Output as JSON')
8236
- .action(async (action, args, opts) => {
8237
- const config = getBrainConfig(opts);
7934
+ brainCmd
7935
+ .command('transfer <source> <target>')
7936
+ .description('Transfer knowledge between domains')
7937
+ .action(async (source, target) => {
7938
+ const opts = brainCmd.opts();
7939
+ const spinner = ora(`Transferring knowledge: ${source} -> ${target}...`).start();
8238
7940
  try {
8239
- let result;
8240
- switch (action) {
8241
- case 'list':
8242
- result = await brainFetch(config, '/v1/pages', { params: { limit: 20 } }).catch(() => ({ pages: [], message: 'Brainpedia endpoint not available' }));
8243
- break;
8244
- case 'get':
8245
- if (!args[0]) { console.error(chalk.red('Usage: brain page get <slug>')); process.exit(1); }
8246
- result = await brainFetch(config, `/v1/pages/${args[0]}`);
8247
- break;
8248
- case 'create':
8249
- if (!args[0]) { console.error(chalk.red('Usage: brain page create <title> [--content <text>]')); process.exit(1); }
8250
- result = await brainFetch(config, '/v1/pages', { body: { title: args[0], content: opts.content || '' } });
8251
- break;
8252
- case 'update':
8253
- if (!args[0]) { console.error(chalk.red('Usage: brain page update <slug> [--content <text>]')); process.exit(1); }
8254
- result = await brainFetch(config, `/v1/pages/${args[0]}/deltas`, { body: { content: opts.content || '' } });
8255
- break;
8256
- case 'delete':
8257
- if (!args[0]) { console.error(chalk.red('Usage: brain page delete <slug>')); process.exit(1); }
8258
- result = await brainFetch(config, `/v1/pages/${args[0]}`, { method: 'DELETE' }).catch(() => ({ error: 'Delete not available' }));
8259
- break;
8260
- default:
8261
- console.error(chalk.red(`Unknown page action: ${action}. Use: list, get, create, update, delete`));
8262
- process.exit(1);
8263
- }
8264
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8265
- if (result.pages) {
8266
- console.log(chalk.bold.cyan('\nBrainpedia Pages\n'));
8267
- if (result.total_count !== undefined) console.log(chalk.dim(` ${result.total_count} total pages\n`));
8268
- result.pages.forEach((p, i) => {
8269
- const score = p.quality_score !== undefined ? chalk.dim(` (${(p.quality_score * 100).toFixed(0)}%)`) : '';
8270
- const status = p.status ? chalk.dim(` [${p.status}]`) : '';
8271
- console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(p.title || p.slug || p.id)}${status}${score}`);
8272
- if (p.category) console.log(` ${chalk.dim(p.category)}`);
8273
- });
8274
- } else if (result.memory && result.memory.title) {
8275
- // Unwrap .memory wrapper from page detail response
8276
- const page = result.memory;
8277
- console.log(chalk.bold.cyan(`\n${page.title}\n`));
8278
- if (result.status) console.log(` ${chalk.bold('Status:')} ${result.status}`);
8279
- if (page.category) console.log(` ${chalk.bold('Category:')} ${page.category}`);
8280
- if (page.quality_score !== undefined) console.log(` ${chalk.bold('Quality:')} ${(page.quality_score * 100).toFixed(0)}%`);
8281
- if (result.delta_count !== undefined) console.log(` ${chalk.bold('Deltas:')} ${result.delta_count}`);
8282
- if (result.evidence_count !== undefined) console.log(` ${chalk.bold('Evidence:')} ${result.evidence_count}`);
8283
- if (page.tags && page.tags.length) console.log(` ${chalk.bold('Tags:')} ${page.tags.join(', ')}`);
8284
- if (page.content) { console.log(); console.log(page.content); }
8285
- } else if (result.title) {
8286
- console.log(chalk.bold.cyan(`\n${result.title}\n`));
8287
- if (result.content) console.log(result.content);
7941
+ const client = await makeBrainClient(opts);
7942
+ const result = await client.transfer(source, target);
7943
+ spinner.succeed(chalk.green('Transfer complete'));
7944
+ if (isJsonOutput(opts)) {
7945
+ console.log(JSON.stringify(result, null, 2));
8288
7946
  } else {
7947
+ console.log(chalk.gray(` Source: ${source}`));
7948
+ console.log(chalk.gray(` Target: ${target}`));
7949
+ if (result.transferred != null) console.log(chalk.gray(` Transferred: ${result.transferred} items`));
7950
+ }
7951
+ } catch (error) {
7952
+ spinner.fail(chalk.red('Transfer failed'));
7953
+ console.error(chalk.red(error.message));
7954
+ process.exit(1);
7955
+ }
7956
+ });
7957
+
7958
+ brainCmd
7959
+ .command('sync [direction]')
7960
+ .description('Sync LoRA weights (pull, push, or both)')
7961
+ .action(async (direction) => {
7962
+ const opts = brainCmd.opts();
7963
+ const dir = direction || 'both';
7964
+ if (!['pull', 'push', 'both'].includes(dir)) {
7965
+ console.error(chalk.red('Direction must be "pull", "push", or "both"'));
7966
+ process.exit(1);
7967
+ }
7968
+ const spinner = ora(`Syncing LoRA weights (${dir})...`).start();
7969
+ try {
7970
+ await requirePiBrain();
7971
+ const url = opts.url || process.env.BRAIN_URL || 'https://pi.ruv.io';
7972
+ const apiKey = process.env.PI || 'anonymous';
7973
+ const headers = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
7974
+ const res = await fetch(`${url}/v1/lora/sync`, {
7975
+ method: 'POST',
7976
+ headers,
7977
+ body: JSON.stringify({ direction: dir }),
7978
+ });
7979
+ if (!res.ok) throw new Error(`Sync failed (${res.status})`);
7980
+ const result = await res.json();
7981
+ spinner.succeed(chalk.green(`LoRA sync complete (${dir})`));
7982
+ if (isJsonOutput(opts)) {
8289
7983
  console.log(JSON.stringify(result, null, 2));
7984
+ } else {
7985
+ console.log(chalk.gray(` Direction: ${dir}`));
7986
+ console.log(chalk.dim(` ${JSON.stringify(result)}`));
8290
7987
  }
8291
- console.log();
8292
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
7988
+ } catch (error) {
7989
+ spinner.fail(chalk.red('LoRA sync failed'));
7990
+ console.error(chalk.red(error.message));
7991
+ process.exit(1);
7992
+ }
8293
7993
  });
8294
7994
 
8295
- brainCmd.command('node <action> [args...]')
8296
- .description('WASM compute node management (publish, list, status)')
8297
- .option('--url <url>', 'Brain server URL')
8298
- .option('--key <key>', 'Pi key')
8299
- .option('--json', 'Output as JSON')
8300
- .action(async (action, args, opts) => {
8301
- const config = getBrainConfig(opts);
7995
+ brainCmd
7996
+ .command('page <action> [id]')
7997
+ .description('Brainpedia page management (list, get, create, update)')
7998
+ .option('--title <title>', 'Page title (for create/update)')
7999
+ .option('--content <content>', 'Page content (for create/update)')
8000
+ .option('-l, --limit <n>', 'Max results for list', '20')
8001
+ .action(async (action, id, cmdOpts) => {
8002
+ const opts = brainCmd.opts();
8003
+ const validActions = ['list', 'get', 'create', 'update'];
8004
+ if (!validActions.includes(action)) {
8005
+ console.error(chalk.red(`Action must be one of: ${validActions.join(', ')}`));
8006
+ process.exit(1);
8007
+ }
8008
+ if (['get', 'update'].includes(action) && !id) {
8009
+ console.error(chalk.red(`Page ID required for "${action}"`));
8010
+ process.exit(1);
8011
+ }
8012
+ const spinner = ora(`Brainpedia: ${action}...`).start();
8302
8013
  try {
8014
+ const url = opts.url || process.env.BRAIN_URL || 'https://pi.ruv.io';
8015
+ const apiKey = process.env.PI || 'anonymous';
8016
+ const headers = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
8303
8017
  let result;
8304
8018
  switch (action) {
8305
- case 'publish':
8306
- if (!args[0]) { console.error(chalk.red('Usage: brain node publish <wasm-file>')); process.exit(1); }
8307
- const wasmPath = path.resolve(args[0]);
8308
- if (!fs.existsSync(wasmPath)) { console.error(chalk.red(`File not found: ${wasmPath}`)); process.exit(1); }
8309
- const wasmBytes = fs.readFileSync(wasmPath);
8310
- result = await brainFetch(config, '/v1/nodes', { body: { name: path.basename(wasmPath, '.wasm'), wasm_base64: wasmBytes.toString('base64') } });
8019
+ case 'list': {
8020
+ const params = new URLSearchParams();
8021
+ params.set('limit', cmdOpts.limit);
8022
+ const res = await fetch(`${url}/v1/pages?${params}`, { headers });
8023
+ if (!res.ok) throw new Error(`List failed (${res.status})`);
8024
+ result = await res.json();
8311
8025
  break;
8312
- case 'list':
8313
- result = await brainFetch(config, '/v1/nodes', { params: { limit: 20 } }).catch(() => ({ nodes: [], message: 'WASM node listing not available' }));
8026
+ }
8027
+ case 'get': {
8028
+ const res = await fetch(`${url}/v1/pages/${id}`, { headers });
8029
+ if (!res.ok) throw new Error(`Get failed (${res.status})`);
8030
+ result = await res.json();
8031
+ break;
8032
+ }
8033
+ case 'create': {
8034
+ if (!cmdOpts.title || !cmdOpts.content) {
8035
+ spinner.fail(chalk.red('--title and --content are required for create'));
8036
+ process.exit(1);
8037
+ }
8038
+ const res = await fetch(`${url}/v1/pages`, {
8039
+ method: 'POST', headers,
8040
+ body: JSON.stringify({ title: cmdOpts.title, content: cmdOpts.content }),
8041
+ });
8042
+ if (!res.ok) throw new Error(`Create failed (${res.status})`);
8043
+ result = await res.json();
8314
8044
  break;
8315
- case 'status':
8316
- if (!args[0]) { console.error(chalk.red('Usage: brain node status <node-id>')); process.exit(1); }
8317
- result = await brainFetch(config, `/v1/nodes/${args[0]}`);
8045
+ }
8046
+ case 'update': {
8047
+ const body = {};
8048
+ if (cmdOpts.title) body.title = cmdOpts.title;
8049
+ if (cmdOpts.content) body.content = cmdOpts.content;
8050
+ const res = await fetch(`${url}/v1/pages/${id}`, {
8051
+ method: 'PUT', headers,
8052
+ body: JSON.stringify(body),
8053
+ });
8054
+ if (!res.ok) throw new Error(`Update failed (${res.status})`);
8055
+ result = await res.json();
8318
8056
  break;
8319
- default:
8320
- console.error(chalk.red(`Unknown node action: ${action}. Use: publish, list, status`));
8321
- process.exit(1);
8322
- }
8323
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result, null, 2)); return; }
8324
- if (result.nodes) {
8325
- console.log(chalk.bold.cyan('\nWASM Compute Nodes\n'));
8326
- result.nodes.forEach((n, i) => console.log(` ${chalk.yellow(i + 1 + '.')} ${chalk.bold(n.name || n.id)} ${chalk.dim(n.status || '')}`));
8327
- } else if (result.id) {
8328
- console.log(chalk.green(`Published node: ${result.id}`));
8329
- } else {
8330
- console.log(JSON.stringify(result, null, 2));
8057
+ }
8331
8058
  }
8332
- console.log();
8333
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8334
- });
8335
-
8336
- // ── Brain AGI Subcommands ── AGI subsystem diagnostics ──────────────────
8337
- const agiCmd = brainCmd.command('agi').description('AGI subsystem diagnostics SONA, GWT, temporal, meta-learning, midstream');
8338
-
8339
- async function fetchBrainEndpoint(config, endpoint) {
8340
- const url = (config.url || 'https://pi.ruv.io') + endpoint;
8341
- const headers = {};
8342
- if (config.key) headers['Authorization'] = `Bearer ${config.key}`;
8343
- const resp = await proxyFetch(url, { headers, signal: AbortSignal.timeout(30000) });
8344
- if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
8345
- if (resp.status === 204 || resp.headers.get('content-length') === '0') return {};
8346
- return resp.json();
8347
- }
8348
-
8349
- agiCmd.command('status')
8350
- .description('Combined AGI + midstream diagnostics from π.ruv.io')
8351
- .option('--url <url>', 'Brain server URL')
8352
- .option('--key <key>', 'Pi key')
8353
- .option('--json', 'Output as JSON')
8354
- .action(async (opts) => {
8355
- const config = getBrainConfig(opts);
8356
- try {
8357
- const status = await fetchBrainEndpoint(config, '/v1/status');
8358
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(status, null, 2)); return; }
8359
- console.log(chalk.bold.cyan('\n π.ruv.io AGI Diagnostics\n'));
8360
- console.log(chalk.bold(' SONA'));
8361
- console.log(` Patterns: ${status.sona_patterns || 0} Trajectories: ${status.sona_trajectories || 0}`);
8362
- console.log(` Background ticks: ${status.sona_background_ticks || 0}`);
8363
- console.log(chalk.bold('\n GWT Attention'));
8364
- console.log(` Workspace load: ${status.gwt_workspace_load || 0}`);
8365
- console.log(` Avg salience: ${status.gwt_avg_salience || 0}`);
8366
- console.log(chalk.bold('\n Temporal'));
8367
- console.log(` Total deltas: ${status.temporal_deltas || 0}`);
8368
- console.log(` Velocity: ${status.knowledge_velocity || 0}/hr`);
8369
- console.log(` Trend: ${status.temporal_trend || 'unknown'}`);
8370
- console.log(chalk.bold('\n Meta-Learning'));
8371
- console.log(` Avg regret: ${status.meta_avg_regret || 0}`);
8372
- console.log(` Plateau: ${status.meta_plateau_status || 'unknown'}`);
8373
- console.log(chalk.bold('\n Midstream'));
8374
- console.log(` Scheduler ticks: ${status.midstream_scheduler_ticks || 0}`);
8375
- console.log(` Attractor categories: ${status.midstream_attractor_categories || 0}`);
8376
- console.log(` Strange-loop: v${status.midstream_strange_loop_version || '?'}`);
8377
- console.log();
8378
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8379
- });
8380
-
8381
- agiCmd.command('sona')
8382
- .description('SONA learning engine — patterns, trajectories, background ticks')
8383
- .option('--url <url>', 'Brain server URL')
8384
- .option('--key <key>', 'Pi key')
8385
- .option('--json', 'Output as JSON')
8386
- .action(async (opts) => {
8387
- const config = getBrainConfig(opts);
8388
- try {
8389
- const data = await fetchBrainEndpoint(config, '/v1/sona/stats');
8390
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8391
- console.log(chalk.bold.cyan('\n SONA Learning Engine\n'));
8392
- Object.entries(data).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8393
- console.log();
8394
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8395
- });
8396
-
8397
- agiCmd.command('temporal')
8398
- .description('Temporal delta tracking — velocity, trend, total deltas')
8399
- .option('--url <url>', 'Brain server URL')
8400
- .option('--key <key>', 'Pi key')
8401
- .option('--json', 'Output as JSON')
8402
- .action(async (opts) => {
8403
- const config = getBrainConfig(opts);
8404
- try {
8405
- const data = await fetchBrainEndpoint(config, '/v1/temporal');
8406
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8407
- console.log(chalk.bold.cyan('\n Temporal Delta Tracking\n'));
8408
- Object.entries(data).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8409
- console.log();
8410
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8411
- });
8412
-
8413
- agiCmd.command('explore')
8414
- .description('Meta-learning exploration — curiosity, regret, plateau, Pareto')
8415
- .option('--url <url>', 'Brain server URL')
8416
- .option('--key <key>', 'Pi key')
8417
- .option('--json', 'Output as JSON')
8418
- .action(async (opts) => {
8419
- const config = getBrainConfig(opts);
8420
- try {
8421
- const data = await fetchBrainEndpoint(config, '/v1/explore');
8422
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8423
- console.log(chalk.bold.cyan('\n Meta-Learning Exploration\n'));
8424
- Object.entries(data).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8425
- console.log();
8426
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8427
- });
8428
-
8429
- agiCmd.command('midstream')
8430
- .description('Midstream platform — scheduler, attractor, solver, strange-loop')
8431
- .option('--url <url>', 'Brain server URL')
8432
- .option('--key <key>', 'Pi key')
8433
- .option('--json', 'Output as JSON')
8434
- .action(async (opts) => {
8435
- const config = getBrainConfig(opts);
8436
- try {
8437
- const data = await fetchBrainEndpoint(config, '/v1/midstream');
8438
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8439
- console.log(chalk.bold.cyan('\n Midstream Platform\n'));
8440
- Object.entries(data).forEach(([k, v]) => {
8441
- if (typeof v === 'object' && v !== null) {
8442
- console.log(` ${chalk.bold(k + ':')}`);
8443
- Object.entries(v).forEach(([sk, sv]) => console.log(` ${chalk.dim(sk + ':')} ${sv}`));
8059
+ spinner.succeed(chalk.green(`Brainpedia ${action} complete`));
8060
+ if (isJsonOutput(opts)) {
8061
+ console.log(JSON.stringify(result, null, 2));
8062
+ } else {
8063
+ if (action === 'list') {
8064
+ const pages = Array.isArray(result) ? result : (result.pages || []);
8065
+ pages.forEach((p, i) => {
8066
+ console.log(chalk.cyan(` ${i + 1}. ${p.title || p.id || 'Untitled'}`));
8067
+ });
8444
8068
  } else {
8445
- console.log(` ${chalk.bold(k + ':')} ${v}`);
8446
- }
8447
- });
8448
- console.log();
8449
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8450
- });
8451
-
8452
- agiCmd.command('flags')
8453
- .description('Show feature flag state from backend')
8454
- .option('--url <url>', 'Brain server URL')
8455
- .option('--key <key>', 'Pi key')
8456
- .option('--json', 'Output as JSON')
8457
- .action(async (opts) => {
8458
- const config = getBrainConfig(opts);
8459
- try {
8460
- const status = await fetchBrainEndpoint(config, '/v1/status');
8461
- const flags = {};
8462
- for (const [k, v] of Object.entries(status)) {
8463
- if (typeof v === 'boolean' || k.startsWith('rvf_') || k.endsWith('_enabled')) {
8464
- flags[k] = v;
8069
+ console.log(chalk.dim(` ${JSON.stringify(result, null, 2)}`));
8465
8070
  }
8466
8071
  }
8467
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(flags, null, 2)); return; }
8468
- console.log(chalk.bold.cyan('\n Feature Flags\n'));
8469
- Object.entries(flags).forEach(([k, v]) => {
8470
- const icon = v ? chalk.green('●') : chalk.red('○');
8471
- console.log(` ${icon} ${chalk.bold(k)}: ${v}`);
8472
- });
8473
- console.log();
8474
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8072
+ } catch (error) {
8073
+ spinner.fail(chalk.red(`Brainpedia ${action} failed`));
8074
+ console.error(chalk.red(error.message));
8075
+ process.exit(1);
8076
+ }
8475
8077
  });
8476
8078
 
8477
8079
  // ============================================================================
8478
- // Midstream CommandsReal-time streaming analysis platform
8080
+ // Edge commandsdistributed compute network
8479
8081
  // ============================================================================
8480
8082
 
8481
- const midstreamCmd = program.command('midstream').description('Real-time streaming analysis — attractor, scheduler, benchmark');
8083
+ const EDGE_GENESIS_URL = 'https://edge-net-genesis-875130704813.us-central1.run.app';
8084
+ const EDGE_DASHBOARD_URL = 'https://edge-net-dashboard-875130704813.us-central1.run.app';
8482
8085
 
8483
- midstreamCmd.command('status')
8484
- .description('Midstream platform overview')
8485
- .option('--url <url>', 'Brain server URL')
8486
- .option('--key <key>', 'Pi key')
8487
- .option('--json', 'Output as JSON')
8488
- .action(async (opts) => {
8489
- const config = getBrainConfig(opts);
8490
- try {
8491
- const data = await fetchBrainEndpoint(config, '/v1/midstream');
8492
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8493
- console.log(chalk.bold.cyan('\n Midstream Platform Status\n'));
8494
- Object.entries(data).forEach(([k, v]) => {
8495
- if (typeof v === 'object' && v !== null) {
8496
- console.log(` ${chalk.bold(k + ':')}`);
8497
- Object.entries(v).forEach(([sk, sv]) => console.log(` ${chalk.dim(sk + ':')} ${sv}`));
8498
- } else {
8499
- console.log(` ${chalk.bold(k + ':')} ${v}`);
8500
- }
8501
- });
8502
- console.log();
8503
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8504
- });
8086
+ const edgeCmd = program
8087
+ .command('edge')
8088
+ .description('Distributed edge compute network — status, tasks, and rUv balance')
8089
+ .option('--json', 'Force JSON output');
8505
8090
 
8506
- midstreamCmd.command('attractor [category]')
8507
- .description('Lyapunov attractor analysis per category')
8508
- .option('--url <url>', 'Brain server URL')
8509
- .option('--key <key>', 'Pi key')
8510
- .option('--json', 'Output as JSON')
8511
- .action(async (category, opts) => {
8512
- const config = getBrainConfig(opts);
8513
- try {
8514
- const data = await fetchBrainEndpoint(config, '/v1/midstream');
8515
- const attractors = data.attractor_categories || data.attractors || {};
8516
- if (category) {
8517
- const entry = typeof attractors === 'object' ? attractors[category] : null;
8518
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(entry || { error: 'Category not found' }, null, 2)); return; }
8519
- if (!entry) { console.log(chalk.yellow(` No attractor data for category: ${category}`)); return; }
8520
- console.log(chalk.bold.cyan(`\n Attractor: ${category}\n`));
8521
- Object.entries(entry).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8091
+ edgeCmd
8092
+ .command('status')
8093
+ .description('Query edge network status from genesis node')
8094
+ .action(async () => {
8095
+ const opts = edgeCmd.opts();
8096
+ const spinner = ora('Querying edge network...').start();
8097
+ try {
8098
+ const res = await fetch(`${EDGE_GENESIS_URL}/status`);
8099
+ if (!res.ok) throw new Error(`Genesis returned ${res.status}`);
8100
+ const result = await res.json();
8101
+ spinner.succeed(chalk.green('Edge network status'));
8102
+ if (isJsonOutput(opts)) {
8103
+ console.log(JSON.stringify(result, null, 2));
8522
8104
  } else {
8523
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(attractors, null, 2)); return; }
8524
- console.log(chalk.bold.cyan('\n Attractor Categories\n'));
8525
- if (typeof attractors === 'object' && Object.keys(attractors).length > 0) {
8526
- Object.entries(attractors).forEach(([cat, info]) => {
8527
- console.log(` ${chalk.yellow(cat + ':')} ${typeof info === 'object' ? JSON.stringify(info) : info}`);
8528
- });
8529
- } else {
8530
- console.log(chalk.dim(` ${typeof attractors === 'number' ? attractors + ' categories tracked' : 'No attractor data available'}`));
8531
- }
8105
+ console.log(chalk.cyan(` Nodes: ${result.total_nodes != null ? result.total_nodes : 'N/A'}`));
8106
+ console.log(chalk.cyan(` Active: ${result.active_nodes != null ? result.active_nodes : 'N/A'}`));
8107
+ console.log(chalk.cyan(` rUv Supply: ${result.ruv_supply != null ? result.ruv_supply : 'N/A'}`));
8108
+ console.log(chalk.cyan(` Phase: ${result.phase || result.sunset_phase || 'N/A'}`));
8109
+ if (result.uptime) console.log(chalk.gray(` Uptime: ${result.uptime}`));
8532
8110
  }
8533
- console.log();
8534
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8111
+ } catch (error) {
8112
+ spinner.fail(chalk.red('Failed to reach edge network'));
8113
+ console.error(chalk.red(error.message));
8114
+ process.exit(1);
8115
+ }
8535
8116
  });
8536
8117
 
8537
- midstreamCmd.command('scheduler')
8538
- .description('Nanosecond scheduler performance metrics')
8539
- .option('--url <url>', 'Brain server URL')
8540
- .option('--key <key>', 'Pi key')
8541
- .option('--json', 'Output as JSON')
8542
- .action(async (opts) => {
8543
- const config = getBrainConfig(opts);
8544
- try {
8545
- const data = await fetchBrainEndpoint(config, '/v1/midstream');
8546
- const sched = data.scheduler || { ticks: data.scheduler_ticks || 0 };
8547
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(sched, null, 2)); return; }
8548
- console.log(chalk.bold.cyan('\n Nanosecond Scheduler\n'));
8549
- if (typeof sched === 'object') {
8550
- Object.entries(sched).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8118
+ edgeCmd
8119
+ .command('join')
8120
+ .description('Join the edge compute network as a node')
8121
+ .action(async () => {
8122
+ console.log(chalk.bold.cyan('\n Edge-Net: Join as Compute Node\n'));
8123
+ console.log(chalk.white(' The edge compute network currently runs in-browser via WASM + Web Workers.'));
8124
+ console.log(chalk.white(' To join as a compute node, open the dashboard in your browser:\n'));
8125
+ console.log(chalk.yellow(` ${EDGE_DASHBOARD_URL}\n`));
8126
+ console.log(chalk.gray(' Node.js headless join is planned for a future release.'));
8127
+ console.log(chalk.gray(' Set PI=<your-key> to use your identity when joining.\n'));
8128
+ });
8129
+
8130
+ edgeCmd
8131
+ .command('balance [nodeId]')
8132
+ .description('Check rUv balance for a node')
8133
+ .action(async (nodeId) => {
8134
+ const opts = edgeCmd.opts();
8135
+ const id = nodeId || process.env.PI || 'anonymous';
8136
+ const spinner = ora('Fetching rUv balance...').start();
8137
+ try {
8138
+ const res = await fetch(`${EDGE_GENESIS_URL}/balance/${encodeURIComponent(id)}`);
8139
+ if (!res.ok) throw new Error(`Balance query failed (${res.status})`);
8140
+ const result = await res.json();
8141
+ spinner.succeed(chalk.green('rUv balance'));
8142
+ if (isJsonOutput(opts)) {
8143
+ console.log(JSON.stringify(result, null, 2));
8551
8144
  } else {
8552
- console.log(` ${chalk.bold('Ticks:')} ${sched}`);
8145
+ console.log(chalk.cyan(` Node: ${id}`));
8146
+ console.log(chalk.cyan(` Balance: ${result.balance != null ? result.balance : JSON.stringify(result)} rUv`));
8553
8147
  }
8554
- console.log();
8555
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8556
- });
8557
-
8558
- midstreamCmd.command('benchmark')
8559
- .description('Run latency benchmark against brain backend')
8560
- .option('--url <url>', 'Brain server URL')
8561
- .option('--key <key>', 'Pi key')
8562
- .option('-n, --concurrent <n>', 'Concurrent search requests', '20')
8563
- .option('--json', 'Output as JSON')
8564
- .action(async (opts) => {
8565
- const config = getBrainConfig(opts);
8566
- const baseUrl = config.url || 'https://pi.ruv.io';
8567
- const headers = {};
8568
- if (config.key) headers['Authorization'] = `Bearer ${config.key}`;
8569
- const concurrentN = Math.min(parseInt(opts.concurrent) || 20, 100);
8570
-
8571
- async function timeRequest(url, label) {
8572
- const start = performance.now();
8573
- const resp = await proxyFetch(url, { headers, signal: AbortSignal.timeout(30000) });
8574
- const elapsed = performance.now() - start;
8575
- return { label, status: resp.status, elapsed };
8576
- }
8577
-
8578
- function percentile(sorted, p) {
8579
- const idx = Math.ceil(sorted.length * p / 100) - 1;
8580
- return sorted[Math.max(0, idx)];
8148
+ } catch (error) {
8149
+ spinner.fail(chalk.red('Failed to fetch balance'));
8150
+ console.error(chalk.red(error.message));
8151
+ process.exit(1);
8581
8152
  }
8153
+ });
8582
8154
 
8583
- try {
8584
- if (!opts.json && process.stdout.isTTY) console.log(chalk.bold.cyan('\n Midstream Benchmark\n'));
8585
-
8586
- // Sequential tests (3 each)
8587
- const endpoints = [
8588
- { path: '/v1/health', label: 'health' },
8589
- { path: '/v1/status', label: 'status' },
8590
- { path: '/v1/memories/search?q=test&limit=3', label: 'search' },
8591
- { path: '/v1/midstream', label: 'midstream' },
8592
- ];
8593
-
8594
- const sequential = {};
8595
- for (const ep of endpoints) {
8596
- const times = [];
8597
- for (let i = 0; i < 3; i++) {
8598
- const r = await timeRequest(baseUrl + ep.path, ep.label);
8599
- times.push(r.elapsed);
8155
+ edgeCmd
8156
+ .command('tasks')
8157
+ .description('List available distributed compute tasks')
8158
+ .action(async () => {
8159
+ const opts = edgeCmd.opts();
8160
+ const spinner = ora('Fetching compute tasks...').start();
8161
+ try {
8162
+ const res = await fetch(`${EDGE_GENESIS_URL}/tasks`);
8163
+ if (!res.ok) throw new Error(`Tasks query failed (${res.status})`);
8164
+ const result = await res.json();
8165
+ const tasks = Array.isArray(result) ? result : (result.tasks || []);
8166
+ spinner.succeed(chalk.green(`${tasks.length} task(s) available`));
8167
+ if (isJsonOutput(opts)) {
8168
+ console.log(JSON.stringify(result, null, 2));
8169
+ } else {
8170
+ if (tasks.length === 0) {
8171
+ console.log(chalk.gray(' No compute tasks currently available.'));
8172
+ } else {
8173
+ tasks.forEach((task, i) => {
8174
+ console.log(chalk.cyan(` ${i + 1}. ${task.name || task.id || 'Task'}`) +
8175
+ (task.reward ? chalk.yellow(` (${task.reward} rUv)`) : '') +
8176
+ (task.status ? chalk.gray(` [${task.status}]`) : ''));
8177
+ });
8600
8178
  }
8601
- sequential[ep.label] = { avg: times.reduce((a, b) => a + b, 0) / times.length, min: Math.min(...times), max: Math.max(...times) };
8602
- }
8603
-
8604
- // Concurrent search test
8605
- const concurrentTimes = [];
8606
- const promises = [];
8607
- for (let i = 0; i < concurrentN; i++) {
8608
- promises.push(timeRequest(baseUrl + '/v1/memories/search?q=test&limit=3', 'concurrent'));
8609
8179
  }
8610
- const results = await Promise.all(promises);
8611
- const sorted = results.map(r => r.elapsed).sort((a, b) => a - b);
8612
- const p50 = percentile(sorted, 50);
8613
- const p90 = percentile(sorted, 90);
8614
- const p99 = percentile(sorted, 99);
8615
-
8616
- const benchResult = { sequential, concurrent: { count: concurrentN, p50, p90, p99 } };
8617
-
8618
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(benchResult, null, 2)); return; }
8619
-
8620
- console.log(chalk.bold(' Sequential (3 rounds each):'));
8621
- for (const [label, data] of Object.entries(sequential)) {
8622
- console.log(` ${chalk.yellow(label.padEnd(12))} avg: ${data.avg.toFixed(1)}ms min: ${data.min.toFixed(1)}ms max: ${data.max.toFixed(1)}ms`);
8623
- }
8624
- console.log(chalk.bold(`\n Concurrent (${concurrentN}x search):`));
8625
- console.log(` p50: ${p50.toFixed(1)}ms p90: ${p90.toFixed(1)}ms p99: ${p99.toFixed(1)}ms`);
8626
- console.log();
8627
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8628
- });
8629
-
8630
- // ============================================================================
8631
- // Edge Commands — Distributed compute via @ruvector/edge-net
8632
- // ============================================================================
8633
-
8634
- const edgeCmd = program.command('edge').description('Distributed P2P compute network — status, join, balance, tasks');
8635
-
8636
- const EDGE_GENESIS = 'https://edge-net-genesis-875130704813.us-central1.run.app';
8637
-
8638
- edgeCmd.command('status')
8639
- .description('Show edge compute network status')
8640
- .option('--json', 'Output as JSON')
8641
- .action(async (opts) => {
8642
- try {
8643
- const resp = await proxyFetch(`${EDGE_GENESIS}/status`, { signal: AbortSignal.timeout(30000) });
8644
- const data = await resp.json();
8645
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8646
- console.log(chalk.bold.cyan('\nEdge Network Status\n'));
8647
- Object.entries(data).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8648
- console.log();
8649
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8650
- });
8651
-
8652
- edgeCmd.command('join')
8653
- .description('Join the edge compute network as a compute node')
8654
- .option('--contribution <level>', 'Contribution level 0.0-1.0', '0.3')
8655
- .action(async (opts) => {
8656
- const piKey = process.env.PI;
8657
- if (!piKey) { console.error(chalk.red('Set PI environment variable first. Run: npx ruvector identity generate')); process.exit(1); }
8658
- try {
8659
- const resp = await proxyFetch(`${EDGE_GENESIS}/join`, {
8660
- method: 'POST',
8661
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${piKey}` },
8662
- body: JSON.stringify({ contribution: parseFloat(opts.contribution), pi_key: piKey }),
8663
- signal: AbortSignal.timeout(30000)
8664
- });
8665
- const data = await resp.json();
8666
- console.log(chalk.green(`Joined edge network: ${data.node_id || 'OK'}`));
8667
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8180
+ } catch (error) {
8181
+ spinner.fail(chalk.red('Failed to fetch tasks'));
8182
+ console.error(chalk.red(error.message));
8183
+ process.exit(1);
8184
+ }
8668
8185
  });
8669
8186
 
8670
- edgeCmd.command('balance')
8671
- .description('Check rUv credit balance')
8672
- .option('--json', 'Output as JSON')
8673
- .action(async (opts) => {
8674
- const piKey = process.env.PI;
8675
- if (!piKey) { console.error(chalk.red('Set PI environment variable first.')); process.exit(1); }
8676
- try {
8677
- const pseudonym = require('crypto').createHash('shake256', { outputLength: 16 }).update(piKey).digest('hex');
8678
- const resp = await proxyFetch(`${EDGE_GENESIS}/balance/${pseudonym}`, { headers: { 'Authorization': `Bearer ${piKey}` }, signal: AbortSignal.timeout(30000) });
8679
- if (!resp.ok) { console.error(chalk.red(`Edge network returned ${resp.status} ${resp.statusText}`)); process.exit(1); }
8680
- const data = await resp.json();
8681
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8682
- console.log(chalk.bold.cyan('\nrUv Balance\n'));
8683
- console.log(` ${chalk.bold('Balance:')} ${data.balance || 0} rUv`);
8684
- console.log();
8685
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8686
- });
8687
-
8688
- edgeCmd.command('tasks')
8689
- .description('List available distributed compute tasks')
8690
- .option('-l, --limit <n>', 'Max tasks', '20')
8691
- .option('--json', 'Output as JSON')
8692
- .action(async (opts) => {
8693
- try {
8694
- const resp = await proxyFetch(`${EDGE_GENESIS}/tasks?limit=${opts.limit}`, { signal: AbortSignal.timeout(30000) });
8695
- const data = await resp.json();
8696
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(data, null, 2)); return; }
8697
- console.log(chalk.bold.cyan('\nEdge Compute Tasks\n'));
8698
- const tasks = Array.isArray(data) ? data : data.tasks || [];
8699
- if (!tasks.length) { console.log(chalk.dim(' No tasks available.\n')); return; }
8700
- tasks.forEach((t, i) => console.log(` ${chalk.yellow(i + 1 + '.')} ${t.name || t.id} ${chalk.dim(`[${t.status || 'pending'}]`)}`));
8701
- console.log();
8702
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8703
- });
8704
-
8705
- edgeCmd.command('dashboard')
8706
- .description('Open edge-net dashboard in browser')
8707
- .action(() => {
8708
- const url = 'https://edge-net-dashboard-875130704813.us-central1.run.app';
8709
- console.log(chalk.cyan(`Dashboard: ${url}`));
8187
+ edgeCmd
8188
+ .command('dashboard')
8189
+ .description('Open the edge-net dashboard in your default browser')
8190
+ .action(async () => {
8191
+ console.log(chalk.cyan(`\n Opening edge-net dashboard: ${EDGE_DASHBOARD_URL}\n`));
8710
8192
  try {
8711
8193
  const { execSync } = require('child_process');
8712
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
8713
- execSync(`${cmd} ${url}`, { stdio: 'ignore' });
8714
- } catch { console.log(chalk.dim(' Open the URL above in your browser.')); }
8194
+ const platform = process.platform;
8195
+ if (platform === 'darwin') {
8196
+ execSync(`open "${EDGE_DASHBOARD_URL}"`);
8197
+ } else if (platform === 'win32') {
8198
+ execSync(`start "" "${EDGE_DASHBOARD_URL}"`);
8199
+ } else {
8200
+ execSync(`xdg-open "${EDGE_DASHBOARD_URL}" 2>/dev/null || echo "Open manually: ${EDGE_DASHBOARD_URL}"`);
8201
+ }
8202
+ } catch {
8203
+ console.log(chalk.yellow(` Could not open browser. Visit manually:`));
8204
+ console.log(chalk.white(` ${EDGE_DASHBOARD_URL}\n`));
8205
+ }
8715
8206
  });
8716
8207
 
8717
8208
  // ============================================================================
8718
- // Identity CommandsPi key management
8209
+ // Identity commandsmanage your pi key
8719
8210
  // ============================================================================
8720
8211
 
8721
- const identityCmd = program.command('identity').description('Pi key identity management — generate, show, export, import');
8722
-
8723
- identityCmd.command('generate')
8724
- .description('Generate a new pi key')
8725
- .action(() => {
8212
+ const identityCmd = program
8213
+ .command('identity')
8214
+ .description('Manage your pi identity key for brain, edge, and MCP')
8215
+ .option('--json', 'Force JSON output');
8216
+
8217
+ identityCmd
8218
+ .command('generate')
8219
+ .description('Generate a new pi identity key (64 hex chars)')
8220
+ .option('--save', 'Save the key to ~/.ruvector/pi-key')
8221
+ .action(async (cmdOpts) => {
8222
+ const opts = identityCmd.opts();
8726
8223
  const crypto = require('crypto');
8727
8224
  const key = crypto.randomBytes(32).toString('hex');
8728
- const hash = crypto.createHash('shake256', { outputLength: 16 });
8729
- hash.update(key);
8730
- const pseudonym = hash.digest('hex');
8731
- console.log(chalk.bold.cyan('\nNew Pi Identity Generated\n'));
8732
- console.log(` ${chalk.bold('Pi Key:')} ${chalk.yellow(key)}`);
8733
- console.log(` ${chalk.bold('Pseudonym:')} ${chalk.green(pseudonym)}`);
8734
- console.log();
8735
- console.log(chalk.dim(' Store securely. Set PI env var to use:'));
8736
- console.log(chalk.cyan(` export PI=${key}\n`));
8225
+ let pseudonym;
8226
+ try {
8227
+ const hash = crypto.createHash('shake256', { outputLength: 16 });
8228
+ hash.update(key);
8229
+ pseudonym = hash.digest('hex');
8230
+ } catch {
8231
+ pseudonym = crypto.createHash('sha256').update(key).digest('hex').substring(0, 32);
8232
+ }
8233
+ if (isJsonOutput(opts)) {
8234
+ console.log(JSON.stringify({ key, pseudonym }, null, 2));
8235
+ } else {
8236
+ console.log(chalk.bold.cyan('\n New Pi Identity Generated\n'));
8237
+ console.log(chalk.white(` Key: ${key}`));
8238
+ console.log(chalk.gray(` Pseudonym: ${pseudonym}`));
8239
+ console.log(chalk.yellow('\n Save this key! Set it as PI environment variable:'));
8240
+ console.log(chalk.dim(` export PI=${key}\n`));
8241
+ }
8242
+ if (cmdOpts.save) {
8243
+ const keyDir = path.join(require('os').homedir(), '.ruvector');
8244
+ const keyPath = path.join(keyDir, 'pi-key');
8245
+ if (!fs.existsSync(keyDir)) fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
8246
+ fs.writeFileSync(keyPath, key, { mode: 0o600 });
8247
+ console.log(chalk.green(` Key saved to ${keyPath}`));
8248
+ }
8737
8249
  });
8738
8250
 
8739
- identityCmd.command('show')
8740
- .description('Show current pi key pseudonym and derived identities')
8741
- .option('--json', 'Output as JSON')
8742
- .action((opts) => {
8743
- const piKey = process.env.PI;
8744
- if (!piKey) {
8745
- console.error(chalk.red('No PI environment variable set.'));
8746
- console.error(chalk.yellow(' Run: npx ruvector identity generate'));
8251
+ identityCmd
8252
+ .command('show')
8253
+ .description('Show current identity pseudonym derived from your pi key')
8254
+ .action(async () => {
8255
+ const opts = identityCmd.opts();
8256
+ const crypto = require('crypto');
8257
+ let key = process.env.PI;
8258
+ if (!key) {
8259
+ const keyPath = path.join(require('os').homedir(), '.ruvector', 'pi-key');
8260
+ if (fs.existsSync(keyPath)) {
8261
+ key = fs.readFileSync(keyPath, 'utf8').trim();
8262
+ }
8263
+ }
8264
+ if (!key) {
8265
+ console.error(chalk.red('No pi key found. Set PI env var or run: ruvector identity generate --save'));
8747
8266
  process.exit(1);
8748
8267
  }
8749
- const crypto = require('crypto');
8750
- const hash = crypto.createHash('shake256', { outputLength: 16 });
8751
- hash.update(piKey);
8752
- const pseudonym = hash.digest('hex');
8753
- const mcpToken = crypto.createHmac('sha256', piKey).update('mcp').digest('hex').slice(0, 32);
8754
- const edgeKeyBuf = crypto.createHash('sha512').update(piKey).update('edge-net').digest().slice(0, 32);
8755
- const edgeKey = edgeKeyBuf.toString('hex');
8756
- if (opts.json || !process.stdout.isTTY) {
8757
- console.log(JSON.stringify({ pseudonym, mcp_token: mcpToken, edge_key: edgeKey, key_prefix: piKey.slice(0, 8) + '...' }, null, 2));
8758
- return;
8268
+ let pseudonym;
8269
+ try {
8270
+ const hash = crypto.createHash('shake256', { outputLength: 16 });
8271
+ hash.update(key);
8272
+ pseudonym = hash.digest('hex');
8273
+ } catch {
8274
+ pseudonym = crypto.createHash('sha256').update(key).digest('hex').substring(0, 32);
8275
+ }
8276
+ if (isJsonOutput(opts)) {
8277
+ console.log(JSON.stringify({
8278
+ pseudonym,
8279
+ key_preview: key.substring(0, 8) + '...' + key.substring(key.length - 8),
8280
+ source: process.env.PI ? 'PI env var' : '~/.ruvector/pi-key',
8281
+ }, null, 2));
8282
+ } else {
8283
+ console.log(chalk.bold.cyan('\n Pi Identity\n'));
8284
+ console.log(chalk.white(` Pseudonym: ${pseudonym}`));
8285
+ console.log(chalk.gray(` Key: ${key.substring(0, 8)}...${key.substring(key.length - 8)}`));
8286
+ console.log(chalk.gray(` Source: ${process.env.PI ? 'PI env var' : '~/.ruvector/pi-key'}\n`));
8759
8287
  }
8760
- console.log(chalk.bold.cyan('\nPi Identity\n'));
8761
- console.log(` ${chalk.bold('Key:')} ${piKey.slice(0, 8)}...${piKey.slice(-8)}`);
8762
- console.log(` ${chalk.bold('Pseudonym:')} ${chalk.green(pseudonym)}`);
8763
- console.log(` ${chalk.bold('MCP Token:')} ${chalk.dim(mcpToken)}`);
8764
- console.log(` ${chalk.bold('Edge Key:')} ${chalk.dim(edgeKey)}`);
8765
- console.log();
8766
8288
  });
8767
8289
 
8768
- identityCmd.command('export <file>')
8769
- .description('Export pi key to encrypted file')
8770
- .action((file) => {
8771
- const piKey = process.env.PI;
8772
- if (!piKey) { console.error(chalk.red('No PI environment variable set.')); process.exit(1); }
8290
+ identityCmd
8291
+ .command('export')
8292
+ .description('Export your pi key encrypted with a passphrase')
8293
+ .option('-o, --output <file>', 'Output file', 'pi-key.enc')
8294
+ .action(async (cmdOpts) => {
8295
+ const opts = identityCmd.opts();
8773
8296
  const crypto = require('crypto');
8774
- const passphrase = crypto.randomBytes(16).toString('hex');
8775
- const key = crypto.scryptSync(passphrase, 'ruvector-pi', 32);
8776
- const iv = crypto.randomBytes(16);
8777
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
8778
- let encrypted = cipher.update(piKey, 'utf8', 'hex');
8297
+ let key = process.env.PI;
8298
+ if (!key) {
8299
+ const keyPath = path.join(require('os').homedir(), '.ruvector', 'pi-key');
8300
+ if (fs.existsSync(keyPath)) {
8301
+ key = fs.readFileSync(keyPath, 'utf8').trim();
8302
+ }
8303
+ }
8304
+ if (!key) {
8305
+ console.error(chalk.red('No pi key found. Set PI env var or run: ruvector identity generate --save'));
8306
+ process.exit(1);
8307
+ }
8308
+ let passphrase = process.env.PI_PASSPHRASE;
8309
+ if (!passphrase) {
8310
+ passphrase = crypto.randomBytes(16).toString('hex');
8311
+ console.log(chalk.yellow(`\n Generated passphrase (save this!): ${passphrase}`));
8312
+ }
8313
+ const salt = crypto.randomBytes(16);
8314
+ const derivedKey = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
8315
+ const iv = crypto.randomBytes(12);
8316
+ const cipher = crypto.createCipheriv('aes-256-gcm', derivedKey, iv);
8317
+ let encrypted = cipher.update(key, 'utf8', 'hex');
8779
8318
  encrypted += cipher.final('hex');
8780
- const tag = cipher.getAuthTag();
8781
- const data = { iv: iv.toString('hex'), tag: tag.toString('hex'), data: encrypted };
8782
- fs.writeFileSync(file, JSON.stringify(data));
8783
- console.log(chalk.green(`Exported to ${file}`));
8784
- console.log(chalk.bold(`Passphrase: ${chalk.yellow(passphrase)}`));
8785
- console.log(chalk.dim(' Store passphrase separately from the export file.\n'));
8319
+ const authTag = cipher.getAuthTag().toString('hex');
8320
+ const payload = JSON.stringify({
8321
+ version: 1,
8322
+ algorithm: 'aes-256-gcm',
8323
+ salt: salt.toString('hex'),
8324
+ iv: iv.toString('hex'),
8325
+ authTag,
8326
+ data: encrypted,
8327
+ });
8328
+ const outFile = path.resolve(cmdOpts.output);
8329
+ fs.writeFileSync(outFile, payload, { mode: 0o600 });
8330
+ if (isJsonOutput(opts)) {
8331
+ console.log(JSON.stringify({ file: outFile, algorithm: 'aes-256-gcm' }, null, 2));
8332
+ } else {
8333
+ console.log(chalk.green(`\n Key exported to: ${outFile}`));
8334
+ console.log(chalk.gray(' Encrypted with AES-256-GCM + PBKDF2'));
8335
+ console.log(chalk.yellow(' Keep your passphrase safe -- it cannot be recovered.\n'));
8336
+ }
8786
8337
  });
8787
8338
 
8788
- identityCmd.command('import <file>')
8789
- .description('Import pi key from encrypted backup')
8790
- .requiredOption('-p, --passphrase <pass>', 'Decryption passphrase')
8791
- .action((file, opts) => {
8339
+ identityCmd
8340
+ .command('import <file>')
8341
+ .description('Import a pi key from an encrypted backup')
8342
+ .option('--save', 'Save the imported key to ~/.ruvector/pi-key')
8343
+ .action(async (file, cmdOpts) => {
8344
+ const opts = identityCmd.opts();
8345
+ const crypto = require('crypto');
8346
+ const filePath = path.resolve(file);
8347
+ if (!fs.existsSync(filePath)) {
8348
+ console.error(chalk.red(`File not found: ${filePath}`));
8349
+ process.exit(1);
8350
+ }
8351
+ const payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
8352
+ if (payload.version !== 1 || payload.algorithm !== 'aes-256-gcm') {
8353
+ console.error(chalk.red('Unsupported key file format'));
8354
+ process.exit(1);
8355
+ }
8356
+ const passphrase = process.env.PI_PASSPHRASE;
8357
+ if (!passphrase) {
8358
+ console.error(chalk.red('Set PI_PASSPHRASE env var to decrypt the key'));
8359
+ process.exit(1);
8360
+ }
8792
8361
  try {
8793
- const crypto = require('crypto');
8794
- const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
8795
- const key = crypto.scryptSync(opts.passphrase, 'ruvector-pi', 32);
8796
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(raw.iv, 'hex'));
8797
- decipher.setAuthTag(Buffer.from(raw.tag, 'hex'));
8798
- let decrypted = decipher.update(raw.data, 'hex', 'utf8');
8362
+ const salt = Buffer.from(payload.salt, 'hex');
8363
+ const iv = Buffer.from(payload.iv, 'hex');
8364
+ const authTag = Buffer.from(payload.authTag, 'hex');
8365
+ const derivedKey = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
8366
+ const decipher = crypto.createDecipheriv('aes-256-gcm', derivedKey, iv);
8367
+ decipher.setAuthTag(authTag);
8368
+ let decrypted = decipher.update(payload.data, 'hex', 'utf8');
8799
8369
  decrypted += decipher.final('utf8');
8800
- console.log(chalk.green('Key imported successfully.'));
8801
- console.log(chalk.cyan(` export PI=${decrypted}\n`));
8802
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8370
+ if (!/^[0-9a-f]{64}$/i.test(decrypted)) {
8371
+ throw new Error('Decrypted value is not a valid pi key');
8372
+ }
8373
+ if (isJsonOutput(opts)) {
8374
+ console.log(JSON.stringify({
8375
+ key: decrypted,
8376
+ source: filePath,
8377
+ saved: !!cmdOpts.save,
8378
+ }, null, 2));
8379
+ } else {
8380
+ console.log(chalk.green('\n Key imported successfully'));
8381
+ console.log(chalk.white(` Key: ${decrypted.substring(0, 8)}...${decrypted.substring(decrypted.length - 8)}`));
8382
+ }
8383
+ if (cmdOpts.save) {
8384
+ const keyDir = path.join(require('os').homedir(), '.ruvector');
8385
+ const keyPath = path.join(keyDir, 'pi-key');
8386
+ if (!fs.existsSync(keyDir)) fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
8387
+ fs.writeFileSync(keyPath, decrypted, { mode: 0o600 });
8388
+ console.log(chalk.green(` Key saved to ${keyPath}`));
8389
+ }
8390
+ if (!isJsonOutput(opts)) {
8391
+ console.log(chalk.yellow('\n Set as environment variable:'));
8392
+ console.log(chalk.dim(` export PI=${decrypted}\n`));
8393
+ }
8394
+ } catch (error) {
8395
+ console.error(chalk.red('Failed to decrypt key -- wrong passphrase?'));
8396
+ console.error(chalk.red(error.message));
8397
+ process.exit(1);
8398
+ }
8803
8399
  });
8804
8400
 
8805
- // ============================================================================
8806
- // LLM Commands Text embeddings via @ruvector/ruvllm (lazy-loaded)
8807
- // ============================================================================
8808
-
8809
- const llmCmd = program.command('llm').description('LLM embeddings and inference via @ruvector/ruvllm');
8401
+ // =============================================================================
8402
+ // LLM Commands - LLM orchestration (lazy: @ruvector/ruvllm)
8403
+ // =============================================================================
8810
8404
 
8811
- function requireRuvllm() {
8812
- try { return require('@ruvector/ruvllm'); } catch {
8813
- console.error(chalk.red('LLM commands require @ruvector/ruvllm'));
8814
- console.error(chalk.yellow(' npm install @ruvector/ruvllm'));
8815
- process.exit(1);
8816
- }
8817
- }
8405
+ const llmCmd = program.command('llm').description('LLM orchestration with SONA adaptive learning');
8818
8406
 
8819
- llmCmd.command('embed <text>')
8820
- .description('Generate text embeddings')
8821
- .option('-m, --model <model>', 'Model name')
8822
- .option('--json', 'Output as JSON')
8823
- .action((text, opts) => {
8824
- const ruvllm = requireRuvllm();
8407
+ llmCmd.command('embed')
8408
+ .description('Generate text embeddings via RuvLLM')
8409
+ .argument('<text>', 'Text to embed')
8410
+ .option('-m, --model <model>', 'Model name', 'minilm')
8411
+ .option('--json', 'JSON output')
8412
+ .action(async (text, opts) => {
8413
+ let ruvllm;
8825
8414
  try {
8826
- const embedding = ruvllm.embed ? ruvllm.embed(text, opts.model) : ruvllm.generateEmbedding(text);
8827
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify({ embedding, dimension: embedding.length })); return; }
8828
- console.log(chalk.bold.cyan('\nEmbedding Generated\n'));
8829
- console.log(` ${chalk.bold('Dimension:')} ${embedding.length}`);
8830
- console.log(` ${chalk.bold('Preview:')} [${embedding.slice(0, 5).map(v => v.toFixed(4)).join(', ')}...]`);
8831
- console.log();
8832
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); process.exit(1); }
8415
+ ruvllm = require('@ruvector/ruvllm');
8416
+ } catch (e) {
8417
+ console.error(chalk.red('LLM commands require @ruvector/ruvllm'));
8418
+ console.error(chalk.yellow(' npm install @ruvector/ruvllm'));
8419
+ console.error(chalk.dim(' or: npx ruvector install ruvllm'));
8420
+ console.error(chalk.dim('\n Tip: use "ruvector embed text <text>" for built-in ONNX embeddings'));
8421
+ process.exit(1);
8422
+ }
8423
+ const spinner = ora('Generating embedding...').start();
8424
+ try {
8425
+ const { performance } = require('perf_hooks');
8426
+ const start = performance.now();
8427
+ const llm = new ruvllm.RuvLLM({ embeddingDim: 384 });
8428
+ const embedding = llm.embed(text);
8429
+ const elapsed = performance.now() - start;
8430
+ spinner.stop();
8431
+ if (opts.json) {
8432
+ console.log(JSON.stringify({ text, model: opts.model, dimension: embedding.length, embedding: Array.from(embedding), timeMs: +elapsed.toFixed(2) }, null, 2));
8433
+ return;
8434
+ }
8435
+ console.log(chalk.bold.cyan('\n RuvLLM Embedding\n'));
8436
+ console.log(chalk.dim(` Text: "${text.length > 60 ? text.slice(0, 60) + '...' : text}"`));
8437
+ console.log(chalk.dim(` Model: ${opts.model}`));
8438
+ console.log(chalk.dim(` Dimension: ${embedding.length}`));
8439
+ console.log(chalk.dim(` Time: ${elapsed.toFixed(1)}ms`));
8440
+ console.log(chalk.dim(` First 5 values: [${Array.from(embedding).slice(0, 5).map(v => v.toFixed(4)).join(', ')}...]`));
8441
+ console.log('');
8442
+ } catch (err) {
8443
+ spinner.fail('Embedding generation failed');
8444
+ console.error(chalk.red(` ${err.message}`));
8445
+ process.exit(1);
8446
+ }
8833
8447
  });
8834
8448
 
8835
8449
  llmCmd.command('models')
8836
8450
  .description('List available LLM models')
8837
- .action(() => {
8838
- const ruvllm = requireRuvllm();
8451
+ .option('--json', 'JSON output')
8452
+ .action(async (opts) => {
8453
+ let ruvllm;
8839
8454
  try {
8840
- if (typeof ruvllm.listModels === 'function') {
8841
- const models = ruvllm.listModels();
8842
- models.forEach(m => console.log(` ${chalk.green(m.name || m)} ${chalk.dim(m.description || '')}`));
8843
- } else {
8844
- console.log(chalk.dim(' Model listing requires @ruvector/ruvllm >=2.1.0'));
8845
- console.log(chalk.dim(' Available: MiniLM-L6 (default embedding model)'));
8455
+ ruvllm = require('@ruvector/ruvllm');
8456
+ } catch (e) {
8457
+ console.error(chalk.red('LLM commands require @ruvector/ruvllm'));
8458
+ console.error(chalk.yellow(' npm install @ruvector/ruvllm'));
8459
+ console.error(chalk.dim(' or: npx ruvector install ruvllm'));
8460
+ process.exit(1);
8461
+ }
8462
+ const models = typeof ruvllm.listModels === 'function' ? ruvllm.listModels() :
8463
+ (ruvllm.RUVLTRA_MODELS ? Object.values(ruvllm.RUVLTRA_MODELS) : []);
8464
+ if (opts.json) {
8465
+ console.log(JSON.stringify(models, null, 2));
8466
+ return;
8467
+ }
8468
+ console.log(chalk.bold.cyan('\n RuvLLM Available Models\n'));
8469
+ if (!models || models.length === 0) {
8470
+ console.log(chalk.yellow(' No models registered in the registry.'));
8471
+ console.log(chalk.dim(' Upgrade @ruvector/ruvllm for model registry support:'));
8472
+ console.log(chalk.dim(' npm install @ruvector/ruvllm@latest\n'));
8473
+ } else {
8474
+ for (const m of models) {
8475
+ console.log(chalk.white(` ${chalk.bold(m.id)}`));
8476
+ console.log(chalk.dim(` ${m.name} - ${m.size} (${m.quantization})`));
8477
+ console.log(chalk.dim(` Use case: ${m.useCase}`));
8478
+ console.log(chalk.dim(` Context: ${m.contextLength} tokens`));
8479
+ console.log('');
8846
8480
  }
8847
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8481
+ }
8482
+ console.log(chalk.dim(` Total: ${models ? models.length : 0} models\n`));
8848
8483
  });
8849
8484
 
8850
8485
  llmCmd.command('benchmark')
8851
8486
  .description('Benchmark LLM inference performance')
8852
8487
  .option('-n, --iterations <n>', 'Number of iterations', '100')
8853
- .action((opts) => {
8854
- const ruvllm = requireRuvllm();
8855
- const n = parseInt(opts.iterations);
8856
- const text = 'The quick brown fox jumps over the lazy dog';
8857
- const times = [];
8858
- for (let i = 0; i < n; i++) {
8488
+ .option('--json', 'JSON output')
8489
+ .action(async (opts) => {
8490
+ let ruvllm;
8491
+ try {
8492
+ ruvllm = require('@ruvector/ruvllm');
8493
+ } catch (e) {
8494
+ console.error(chalk.red('LLM commands require @ruvector/ruvllm'));
8495
+ console.error(chalk.yellow(' npm install @ruvector/ruvllm'));
8496
+ console.error(chalk.dim(' or: npx ruvector install ruvllm'));
8497
+ process.exit(1);
8498
+ }
8499
+ const iterations = parseInt(opts.iterations, 10) || 100;
8500
+ const spinner = ora(`Running ${iterations} embedding iterations...`).start();
8501
+ try {
8502
+ const { performance } = require('perf_hooks');
8503
+ const llm = new ruvllm.RuvLLM({ embeddingDim: 384 });
8504
+ const testText = 'The quick brown fox jumps over the lazy dog';
8505
+ for (let i = 0; i < 5; i++) llm.embed(testText);
8506
+ const times = [];
8859
8507
  const start = performance.now();
8860
- ruvllm.embed ? ruvllm.embed(text) : ruvllm.generateEmbedding(text);
8861
- times.push(performance.now() - start);
8862
- }
8863
- times.sort((a, b) => a - b);
8864
- console.log(chalk.bold.cyan('\nLLM Benchmark\n'));
8865
- console.log(` ${chalk.bold('Iterations:')} ${n}`);
8866
- console.log(` ${chalk.bold('P50:')} ${times[Math.floor(n * 0.5)].toFixed(2)}ms`);
8867
- console.log(` ${chalk.bold('P95:')} ${times[Math.floor(n * 0.95)].toFixed(2)}ms`);
8868
- console.log(` ${chalk.bold('P99:')} ${times[Math.floor(n * 0.99)].toFixed(2)}ms`);
8869
- console.log(` ${chalk.bold('Mean:')} ${(times.reduce((a, b) => a + b, 0) / n).toFixed(2)}ms`);
8870
- console.log();
8508
+ for (let i = 0; i < iterations; i++) {
8509
+ const t0 = performance.now();
8510
+ llm.embed(testText);
8511
+ times.push(performance.now() - t0);
8512
+ }
8513
+ const totalMs = performance.now() - start;
8514
+ times.sort((a, b) => a - b);
8515
+ const avgMs = totalMs / iterations;
8516
+ const p50 = times[Math.floor(iterations * 0.5)];
8517
+ const p95 = times[Math.floor(iterations * 0.95)];
8518
+ const p99 = times[Math.floor(iterations * 0.99)];
8519
+ const opsPerSec = (iterations / totalMs) * 1000;
8520
+ spinner.stop();
8521
+ if (opts.json) {
8522
+ console.log(JSON.stringify({ iterations, totalMs: +totalMs.toFixed(2), avgMs: +avgMs.toFixed(3), p50: +p50.toFixed(3), p95: +p95.toFixed(3), p99: +p99.toFixed(3), opsPerSec: +opsPerSec.toFixed(1) }, null, 2));
8523
+ return;
8524
+ }
8525
+ console.log(chalk.bold.cyan('\n RuvLLM Benchmark Results\n'));
8526
+ console.log(chalk.white(` Iterations: ${iterations}`));
8527
+ console.log(chalk.white(` Total time: ${totalMs.toFixed(1)}ms`));
8528
+ console.log(chalk.white(` Avg latency: ${avgMs.toFixed(3)}ms`));
8529
+ console.log(chalk.white(` P50: ${p50.toFixed(3)}ms`));
8530
+ console.log(chalk.white(` P95: ${p95.toFixed(3)}ms`));
8531
+ console.log(chalk.white(` P99: ${p99.toFixed(3)}ms`));
8532
+ console.log(chalk.green(` Throughput: ${opsPerSec.toFixed(1)} ops/sec`));
8533
+ console.log('');
8534
+ } catch (err) {
8535
+ spinner.fail('Benchmark failed');
8536
+ console.error(chalk.red(` ${err.message}`));
8537
+ process.exit(1);
8538
+ }
8871
8539
  });
8872
8540
 
8873
8541
  llmCmd.command('info')
8874
- .description('Show RuvLLM module information')
8875
- .action(() => {
8876
- const ruvllm = requireRuvllm();
8877
- console.log(chalk.bold.cyan('\nRuvLLM Info\n'));
8878
- console.log(` ${chalk.bold('Version:')} ${typeof ruvllm.version === 'function' ? ruvllm.version() : ruvllm.version || 'unknown'}`);
8879
- console.log(` ${chalk.bold('SIMD:')} ${ruvllm.simdEnabled ? 'enabled' : 'not detected'}`);
8880
- console.log();
8542
+ .description('Show LLM module information')
8543
+ .option('--json', 'JSON output')
8544
+ .action(async (opts) => {
8545
+ let ruvllm;
8546
+ try {
8547
+ ruvllm = require('@ruvector/ruvllm');
8548
+ } catch (e) {
8549
+ console.error(chalk.red('LLM commands require @ruvector/ruvllm'));
8550
+ console.error(chalk.yellow(' npm install @ruvector/ruvllm'));
8551
+ console.error(chalk.dim(' or: npx ruvector install ruvllm'));
8552
+ process.exit(1);
8553
+ }
8554
+ const version = typeof ruvllm.version === 'function' ? ruvllm.version() : (ruvllm.version || 'unknown');
8555
+ const hasSIMD = ruvllm.hasSimdSupport ? ruvllm.hasSimdSupport() : false;
8556
+ const models = ruvllm.listModels ? ruvllm.listModels() : [];
8557
+ const info = {
8558
+ package: '@ruvector/ruvllm',
8559
+ version,
8560
+ simd: hasSIMD,
8561
+ availableModels: models.length,
8562
+ features: ['SONA adaptive learning', 'HNSW memory', 'FastGRNN routing', 'SIMD inference', 'LoRA adapters', 'Session management', 'Federated learning']
8563
+ };
8564
+ if (opts.json) {
8565
+ console.log(JSON.stringify(info, null, 2));
8566
+ return;
8567
+ }
8568
+ console.log(chalk.bold.cyan('\n RuvLLM Module Information\n'));
8569
+ console.log(chalk.white(` Package: ${info.package}`));
8570
+ console.log(chalk.white(` Version: ${info.version}`));
8571
+ console.log(chalk.white(` SIMD: ${hasSIMD ? chalk.green('Available') : chalk.yellow('Not available')}`));
8572
+ console.log(chalk.white(` Models: ${info.availableModels} registered`));
8573
+ console.log(chalk.white('\n Features:'));
8574
+ for (const f of info.features) {
8575
+ console.log(chalk.dim(` - ${f}`));
8576
+ }
8577
+ console.log('');
8881
8578
  });
8882
8579
 
8883
- // ============================================================================
8884
- // SONA Commands Self-Optimizing Neural Architecture (lazy-loaded)
8885
- // ============================================================================
8886
-
8887
- const sonaCmd = program.command('sona').description('SONA adaptive learning — status, patterns, train, export');
8888
-
8889
- function loadSona() {
8890
- try { return require('@ruvector/sona'); } catch {
8891
- console.error(chalk.red('SONA commands require @ruvector/sona'));
8892
- console.error(chalk.yellow(' npm install @ruvector/sona'));
8893
- process.exit(1);
8894
- }
8895
- }
8580
+ // =============================================================================
8581
+ // SONA Commands - Self-Optimizing Neural Architecture (bundled dep)
8582
+ // =============================================================================
8896
8583
 
8897
- const SONA_DEFAULT_DIM = 128;
8898
- function createSonaEngine(sona) {
8899
- if (sona.SonaEngine) return new sona.SonaEngine(SONA_DEFAULT_DIM);
8900
- if (sona.SonaCoordinator) return new sona.SonaCoordinator(SONA_DEFAULT_DIM);
8901
- throw new Error('No SONA engine class found');
8902
- }
8903
- function parseSonaResult(val) {
8904
- if (typeof val === 'string') { try { return JSON.parse(val); } catch { return val; } }
8905
- return val;
8906
- }
8584
+ const sonaCmd = program.command('sona').description('SONA Self-Optimizing Neural Architecture');
8907
8585
 
8908
8586
  sonaCmd.command('status')
8909
- .description('Show SONA learning engine status')
8910
- .option('--json', 'Output as JSON')
8911
- .action((opts) => {
8912
- const sona = loadSona();
8587
+ .description('Show SONA learning status')
8588
+ .option('--json', 'JSON output')
8589
+ .action(async (opts) => {
8913
8590
  try {
8914
- const engine = createSonaEngine(sona);
8915
- const status = engine.getStatus ? parseSonaResult(engine.getStatus()) : { enabled: engine.isEnabled ? engine.isEnabled() : true, ...parseSonaResult(engine.getStats ? engine.getStats() : {}) };
8916
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(status, null, 2)); return; }
8917
- console.log(chalk.bold.cyan('\nSONA Status\n'));
8918
- Object.entries(status).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8919
- console.log();
8920
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8591
+ const { SonaEngine } = require('@ruvector/sona');
8592
+ const engine = new SonaEngine();
8593
+ const stats = engine.stats();
8594
+ if (opts.json) { console.log(JSON.stringify(stats, null, 2)); return; }
8595
+ console.log(chalk.bold.cyan('\n SONA Learning Status\n'));
8596
+ console.log(chalk.white(` Trajectories: ${stats.trajectoryCount || stats.trajectory_count || 0}`));
8597
+ console.log(chalk.white(` Patterns: ${stats.patternCount || stats.pattern_count || 0}`));
8598
+ console.log(chalk.white(` Energy: ${typeof stats.energy === 'number' ? stats.energy.toFixed(4) : stats.energy || 'N/A'}`));
8599
+ console.log(chalk.white(` State: ${stats.state || 'initialized'}`));
8600
+ console.log('');
8601
+ } catch (e) {
8602
+ try {
8603
+ const { SonaCoordinator } = require('@ruvector/ruvllm');
8604
+ const sona = new SonaCoordinator();
8605
+ const stats = sona.stats();
8606
+ if (opts.json) { console.log(JSON.stringify(stats, null, 2)); return; }
8607
+ console.log(chalk.bold.cyan('\n SONA Learning Status (JS fallback)\n'));
8608
+ console.log(chalk.white(` Signals received: ${stats.signalsReceived}`));
8609
+ console.log(chalk.white(` Trajectories buffered: ${stats.trajectoriesBuffered}`));
8610
+ console.log(chalk.white(` Total patterns: ${stats.patterns.totalPatterns}`));
8611
+ console.log(chalk.white(` Avg success rate: ${(stats.patterns.avgSuccessRate * 100).toFixed(1)}%`));
8612
+ console.log(chalk.white(` EWC tasks learned: ${stats.ewc.tasksLearned}`));
8613
+ console.log('');
8614
+ } catch (e2) {
8615
+ console.error(chalk.red('SONA native binding not available for this platform.'));
8616
+ console.error(chalk.yellow(' Install @ruvector/ruvllm for JS fallback: npm install @ruvector/ruvllm'));
8617
+ console.error(chalk.dim(` Native error: ${e.message}`));
8618
+ process.exit(1);
8619
+ }
8620
+ }
8921
8621
  });
8922
8622
 
8923
- sonaCmd.command('patterns <query>')
8623
+ sonaCmd.command('patterns')
8924
8624
  .description('Search learned patterns')
8925
- .option('-t, --threshold <n>', 'Similarity threshold', '0.5')
8926
- .option('--json', 'Output as JSON')
8927
- .action((query, opts) => {
8928
- const sona = loadSona();
8929
- try {
8930
- const engine = createSonaEngine(sona);
8931
- const patterns = engine.findPatterns ? engine.findPatterns(query, { threshold: parseFloat(opts.threshold) }) : [];
8932
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(patterns, null, 2)); return; }
8933
- console.log(chalk.bold.cyan('\nLearned Patterns\n'));
8934
- if (!patterns.length) { console.log(chalk.dim(' No patterns found.\n')); return; }
8935
- patterns.forEach((p, i) => console.log(` ${chalk.yellow(i + 1 + '.')} ${p.name || p.pattern || JSON.stringify(p).slice(0, 80)}`));
8936
- console.log();
8937
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8625
+ .argument('<query>', 'Search query')
8626
+ .option('-k, --top-k <n>', 'Number of results', '5')
8627
+ .option('--json', 'JSON output')
8628
+ .action(async (query, opts) => {
8629
+ const topK = parseInt(opts.topK, 10) || 5;
8630
+ try {
8631
+ const { SonaEngine } = require('@ruvector/sona');
8632
+ const engine = new SonaEngine();
8633
+ const results = engine.searchPatterns ? engine.searchPatterns(query, topK) : [];
8634
+ if (opts.json) { console.log(JSON.stringify(results, null, 2)); return; }
8635
+ console.log(chalk.bold.cyan(`\n SONA Pattern Search: "${query}"\n`));
8636
+ if (!results || results.length === 0) {
8637
+ console.log(chalk.yellow(' No patterns found. Record some training data first.'));
8638
+ } else {
8639
+ for (let i = 0; i < results.length; i++) {
8640
+ const r = results[i];
8641
+ console.log(chalk.white(` ${i + 1}. ${chalk.bold(r.type || r.id || 'pattern')}`));
8642
+ console.log(chalk.dim(` Score: ${typeof r.score === 'number' ? r.score.toFixed(4) : r.score || 'N/A'}`));
8643
+ if (r.metadata) console.log(chalk.dim(` Metadata: ${JSON.stringify(r.metadata)}`));
8644
+ console.log('');
8645
+ }
8646
+ }
8647
+ } catch (e) {
8648
+ try {
8649
+ const { SonaCoordinator } = require('@ruvector/ruvllm');
8650
+ const sona = new SonaCoordinator();
8651
+ const bank = sona.getReasoningBank();
8652
+ const embedding = new Float64Array(64);
8653
+ for (let i = 0; i < query.length && i < 64; i++) embedding[i] = query.charCodeAt(i) / 255;
8654
+ const results = bank.findSimilar(Array.from(embedding), topK);
8655
+ if (opts.json) { console.log(JSON.stringify(results, null, 2)); return; }
8656
+ console.log(chalk.bold.cyan(`\n SONA Pattern Search (JS): "${query}"\n`));
8657
+ if (!results || results.length === 0) {
8658
+ console.log(chalk.yellow(' No patterns found. Record some training data first.'));
8659
+ } else {
8660
+ for (let i = 0; i < results.length; i++) {
8661
+ const r = results[i];
8662
+ console.log(chalk.white(` ${i + 1}. ${chalk.bold(r.type || r.id)}`));
8663
+ console.log(chalk.dim(` Success rate: ${(r.successRate * 100).toFixed(1)}%`));
8664
+ console.log('');
8665
+ }
8666
+ }
8667
+ } catch (e2) {
8668
+ console.error(chalk.red('SONA not available.'));
8669
+ console.error(chalk.dim(` Native error: ${e.message}`));
8670
+ process.exit(1);
8671
+ }
8672
+ }
8938
8673
  });
8939
8674
 
8940
- sonaCmd.command('train <data>')
8675
+ sonaCmd.command('train')
8941
8676
  .description('Record a training trajectory')
8942
- .option('--outcome <outcome>', 'Outcome (success/failure)', 'success')
8943
- .action((data, opts) => {
8944
- const sona = loadSona();
8677
+ .argument('<data>', 'Training data JSON file or JSON string')
8678
+ .option('--json', 'JSON output')
8679
+ .action(async (data, opts) => {
8680
+ let trajectoryData;
8945
8681
  try {
8946
- const engine = createSonaEngine(sona);
8947
- if (engine.recordTrajectory) { engine.recordTrajectory(data, opts.outcome); }
8948
- else if (engine.train) { engine.train(data); }
8949
- console.log(chalk.green('Training trajectory recorded.'));
8950
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8682
+ if (fs.existsSync(data)) { trajectoryData = JSON.parse(fs.readFileSync(data, 'utf8')); }
8683
+ else { trajectoryData = JSON.parse(data); }
8684
+ } catch (e) {
8685
+ console.error(chalk.red('Invalid training data. Provide a JSON file path or JSON string.'));
8686
+ console.error(chalk.dim(` Error: ${e.message}`));
8687
+ console.error(chalk.dim('\n Example: ruvector sona train \'{"steps":[{"type":"query","input":"test","output":"result","confidence":0.9}],"outcome":"success"}\''));
8688
+ process.exit(1);
8689
+ }
8690
+ const spinner = ora('Recording trajectory...').start();
8691
+ try {
8692
+ const { SonaEngine } = require('@ruvector/sona');
8693
+ const engine = new SonaEngine();
8694
+ const result = engine.recordTrajectory ? engine.recordTrajectory(JSON.stringify(trajectoryData)) : { recorded: true };
8695
+ spinner.succeed('Trajectory recorded');
8696
+ if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; }
8697
+ console.log(chalk.dim(` Steps: ${trajectoryData.steps ? trajectoryData.steps.length : 'N/A'}`));
8698
+ console.log(chalk.dim(` Outcome: ${trajectoryData.outcome || 'N/A'}`));
8699
+ console.log('');
8700
+ } catch (e) {
8701
+ try {
8702
+ const { SonaCoordinator, TrajectoryBuilder } = require('@ruvector/ruvllm');
8703
+ const sona = new SonaCoordinator();
8704
+ const builder = new TrajectoryBuilder();
8705
+ if (trajectoryData.steps && Array.isArray(trajectoryData.steps)) {
8706
+ for (const step of trajectoryData.steps) { builder.startStep(step.type || 'query', step.input || ''); builder.endStep(step.output || '', step.confidence || 0.5); }
8707
+ }
8708
+ const trajectory = builder.complete(trajectoryData.outcome || 'success');
8709
+ sona.recordTrajectory(trajectory);
8710
+ spinner.succeed('Trajectory recorded (JS fallback)');
8711
+ if (opts.json) { console.log(JSON.stringify({ id: trajectory.id, steps: trajectory.steps.length, outcome: trajectory.outcome }, null, 2)); return; }
8712
+ console.log(chalk.dim(` ID: ${trajectory.id}`));
8713
+ console.log(chalk.dim(` Steps: ${trajectory.steps.length}`));
8714
+ console.log(chalk.dim(` Outcome: ${trajectory.outcome}`));
8715
+ console.log('');
8716
+ } catch (e2) { spinner.fail('Failed to record trajectory'); console.error(chalk.red(` ${e.message}`)); process.exit(1); }
8717
+ }
8951
8718
  });
8952
8719
 
8953
8720
  sonaCmd.command('export')
8954
- .description('Export SONA learned weights to JSON')
8955
- .option('-o, --output <file>', 'Output file', 'sona-weights.json')
8956
- .action((opts) => {
8957
- const sona = loadSona();
8958
- try {
8959
- const engine = createSonaEngine(sona);
8960
- const weights = parseSonaResult(engine.exportWeights ? engine.exportWeights() : engine.getStats ? engine.getStats() : {});
8961
- fs.writeFileSync(opts.output, JSON.stringify(weights, null, 2));
8962
- console.log(chalk.green(`Exported to ${opts.output}`));
8963
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8721
+ .description('Export SONA learned weights')
8722
+ .option('-o, --output <path>', 'Output file', 'sona-export.json')
8723
+ .option('--json', 'JSON output')
8724
+ .action(async (opts) => {
8725
+ const spinner = ora('Exporting SONA state...').start();
8726
+ try {
8727
+ const { SonaEngine } = require('@ruvector/sona');
8728
+ const engine = new SonaEngine();
8729
+ const exported = engine.export ? engine.export() : JSON.stringify(engine.stats());
8730
+ const outputData = typeof exported === 'string' ? exported : JSON.stringify(exported, null, 2);
8731
+ if (opts.json) { spinner.stop(); console.log(outputData); return; }
8732
+ fs.writeFileSync(opts.output, outputData);
8733
+ spinner.succeed(`Exported to ${opts.output}`);
8734
+ console.log(chalk.dim(` Size: ${Buffer.byteLength(outputData)} bytes`));
8735
+ console.log('');
8736
+ } catch (e) {
8737
+ try {
8738
+ const { SonaCoordinator } = require('@ruvector/ruvllm');
8739
+ const sona = new SonaCoordinator();
8740
+ const exportData = JSON.stringify({ stats: sona.stats(), reasoningBank: sona.getReasoningBank().stats(), exportedAt: new Date().toISOString() }, null, 2);
8741
+ if (opts.json) { spinner.stop(); console.log(exportData); return; }
8742
+ fs.writeFileSync(opts.output, exportData);
8743
+ spinner.succeed(`Exported to ${opts.output} (JS fallback)`);
8744
+ console.log(chalk.dim(` Size: ${Buffer.byteLength(exportData)} bytes`));
8745
+ console.log('');
8746
+ } catch (e2) { spinner.fail('Export failed'); console.error(chalk.red(` ${e.message}`)); process.exit(1); }
8747
+ }
8964
8748
  });
8965
8749
 
8966
8750
  sonaCmd.command('stats')
8967
- .description('Show detailed SONA learning statistics')
8968
- .option('--json', 'Output as JSON')
8969
- .action((opts) => {
8970
- const sona = loadSona();
8751
+ .description('Show detailed learning statistics')
8752
+ .option('--json', 'JSON output')
8753
+ .action(async (opts) => {
8971
8754
  try {
8972
- const engine = createSonaEngine(sona);
8973
- const stats = parseSonaResult(engine.getStats ? engine.getStats() : engine.stats ? engine.stats() : {});
8974
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(stats, null, 2)); return; }
8975
- console.log(chalk.bold.cyan('\nSONA Statistics\n'));
8976
- Object.entries(stats).forEach(([k, v]) => console.log(` ${chalk.bold(k + ':')} ${v}`));
8977
- console.log();
8978
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8755
+ const { SonaEngine } = require('@ruvector/sona');
8756
+ const engine = new SonaEngine();
8757
+ const stats = engine.stats();
8758
+ if (opts.json) { console.log(JSON.stringify(stats, null, 2)); return; }
8759
+ console.log(chalk.bold.cyan('\n SONA Detailed Statistics\n'));
8760
+ for (const [key, value] of Object.entries(stats)) {
8761
+ const label = key.replace(/([A-Z])/g, ' $1').replace(/_/g, ' ').trim();
8762
+ const displayLabel = label.charAt(0).toUpperCase() + label.slice(1);
8763
+ if (typeof value === 'object' && value !== null) {
8764
+ console.log(chalk.white(` ${displayLabel}:`));
8765
+ for (const [k, v] of Object.entries(value)) console.log(chalk.dim(` ${k}: ${typeof v === 'number' ? v.toFixed(4) : v}`));
8766
+ } else { console.log(chalk.white(` ${displayLabel}: ${typeof value === 'number' ? value.toFixed(4) : value}`)); }
8767
+ }
8768
+ console.log('');
8769
+ } catch (e) {
8770
+ try {
8771
+ const { SonaCoordinator } = require('@ruvector/ruvllm');
8772
+ const stats = new SonaCoordinator().stats();
8773
+ if (opts.json) { console.log(JSON.stringify(stats, null, 2)); return; }
8774
+ console.log(chalk.bold.cyan('\n SONA Detailed Statistics (JS)\n'));
8775
+ console.log(chalk.white(` Signals received: ${stats.signalsReceived}`));
8776
+ console.log(chalk.white(` Trajectories buffered: ${stats.trajectoriesBuffered}`));
8777
+ console.log(chalk.white('\n Reasoning Bank:'));
8778
+ console.log(chalk.dim(` Total patterns: ${stats.patterns.totalPatterns}`));
8779
+ console.log(chalk.dim(` Avg success rate: ${(stats.patterns.avgSuccessRate * 100).toFixed(1)}%`));
8780
+ if (stats.patterns.byType) { for (const [type, count] of Object.entries(stats.patterns.byType)) console.log(chalk.dim(` ${type}: ${count}`)); }
8781
+ console.log(chalk.white('\n EWC++ (Memory Protection):'));
8782
+ console.log(chalk.dim(` Tasks learned: ${stats.ewc.tasksLearned}`));
8783
+ console.log(chalk.dim(` Lambda: ${stats.ewc.lambda}`));
8784
+ console.log(chalk.dim(` Forgetting rate: ${(stats.ewc.estimatedForgettingRate * 100).toFixed(2)}%`));
8785
+ console.log('');
8786
+ } catch (e2) { console.error(chalk.red('SONA not available.')); console.error(chalk.dim(` Native error: ${e.message}`)); process.exit(1); }
8787
+ }
8979
8788
  });
8980
8789
 
8981
8790
  sonaCmd.command('info')
8982
- .description('Show SONA module availability')
8983
- .action(() => {
8984
- const sona = loadSona();
8985
- console.log(chalk.bold.cyan('\nSONA Info\n'));
8986
- console.log(` ${chalk.bold('Version:')} ${typeof sona.version === 'function' ? sona.version() : sona.version || 'unknown'}`);
8987
- console.log(` ${chalk.bold('Engine:')} ${sona.SonaEngine ? 'Native' : 'JS Fallback'}`);
8988
- console.log();
8791
+ .description('Show SONA module information')
8792
+ .option('--json', 'JSON output')
8793
+ .action(async (opts) => {
8794
+ let nativeAvailable = false, nativeVersion = 'N/A';
8795
+ try { const sona = require('@ruvector/sona'); nativeAvailable = !!sona.SonaEngine; nativeVersion = sona.version || '0.1.4'; } catch (e) { /* not available */ }
8796
+ let jsAvailable = false, jsVersion = 'N/A';
8797
+ try { const ruvllm = require('@ruvector/ruvllm'); jsAvailable = !!ruvllm.SonaCoordinator; jsVersion = typeof ruvllm.version === 'function' ? ruvllm.version() : (ruvllm.version || 'unknown'); } catch (e) { /* not available */ }
8798
+ const info = { package: '@ruvector/sona', nativeBinding: nativeAvailable, nativeVersion, jsFallback: jsAvailable, jsFallbackVersion: jsVersion, features: ['LoRA adaptive weights', 'EWC++ memory protection', 'ReasoningBank pattern storage', 'Trajectory tracking', 'Continual learning', 'Sub-millisecond overhead'] };
8799
+ if (opts.json) { console.log(JSON.stringify(info, null, 2)); return; }
8800
+ console.log(chalk.bold.cyan('\n SONA Module Information\n'));
8801
+ console.log(chalk.white(` Package: ${info.package}`));
8802
+ console.log(chalk.white(` Native binding: ${nativeAvailable ? chalk.green('Available') + chalk.dim(` (v${nativeVersion})`) : chalk.yellow('Not available')}`));
8803
+ console.log(chalk.white(` JS fallback: ${jsAvailable ? chalk.green('Available') + chalk.dim(` (ruvllm v${jsVersion})`) : chalk.yellow('Not available')}`));
8804
+ console.log(chalk.white('\n Features:'));
8805
+ for (const f of info.features) console.log(chalk.dim(` - ${f}`));
8806
+ console.log('');
8989
8807
  });
8990
8808
 
8991
- // ============================================================================
8992
- // Route Commands Semantic routing via @ruvector/router (lazy-loaded)
8993
- // ============================================================================
8994
-
8995
- const routeCmd = program.command('route').description('Semantic routing — classify inputs to routes via HNSW + SIMD');
8996
-
8997
- function requireRouter() {
8998
- try { return require('@ruvector/router'); } catch {
8999
- console.error(chalk.red('Route commands require @ruvector/router'));
9000
- console.error(chalk.yellow(' npm install @ruvector/router'));
9001
- process.exit(1);
9002
- }
9003
- }
8809
+ // =============================================================================
8810
+ // Route Commands - Semantic routing (lazy: @ruvector/router)
8811
+ // =============================================================================
9004
8812
 
9005
- routeCmd.command('classify <input>')
9006
- .description('Classify input to a semantic route')
9007
- .option('--json', 'Output as JSON')
9008
- .action((input, opts) => {
9009
- const router = requireRouter();
9010
- try {
9011
- const result = router.classify ? router.classify(input) : { route: 'default', confidence: 1.0 };
9012
- if (opts.json || !process.stdout.isTTY) { console.log(JSON.stringify(result)); return; }
9013
- console.log(chalk.bold.cyan('\nRoute Classification\n'));
9014
- console.log(` ${chalk.bold('Input:')} ${input}`);
9015
- console.log(` ${chalk.bold('Route:')} ${chalk.green(result.route)}`);
9016
- console.log(` ${chalk.bold('Confidence:')} ${result.confidence}`);
9017
- console.log();
9018
- } catch (e) { console.error(chalk.red(`Error: ${e.message}`)); }
8813
+ const routeCmd = program.command('route').description('Semantic routing for AI agents');
8814
+
8815
+ routeCmd.command('classify')
8816
+ .description('Classify input to a route')
8817
+ .argument('<input>', 'Input text to classify')
8818
+ .option('-r, --routes <json>', 'Routes definition JSON file')
8819
+ .option('-k, --top-k <n>', 'Number of results', '3')
8820
+ .option('--json', 'JSON output')
8821
+ .action(async (input, opts) => {
8822
+ let router;
8823
+ try { router = require('@ruvector/router'); } catch (e) {
8824
+ console.error(chalk.red('Route commands require @ruvector/router'));
8825
+ console.error(chalk.yellow(' npm install @ruvector/router'));
8826
+ console.error(chalk.dim(' or: npx ruvector install router'));
8827
+ process.exit(1);
8828
+ }
8829
+ const topK = parseInt(opts.topK, 10) || 3;
8830
+ const spinner = ora('Classifying input...').start();
8831
+ try {
8832
+ let routesDef = null;
8833
+ if (opts.routes) {
8834
+ if (!fs.existsSync(opts.routes)) { spinner.fail(`Routes file not found: ${opts.routes}`); process.exit(1); }
8835
+ routesDef = JSON.parse(fs.readFileSync(opts.routes, 'utf8'));
8836
+ }
8837
+ const sr = new router.SemanticRouter({ dimension: routesDef ? (routesDef.dimension || 384) : 384 });
8838
+ if (routesDef && routesDef.intents) { for (const intent of routesDef.intents) sr.addIntent(intent); }
8839
+ const embedding = new Float32Array(sr._dimension || 384);
8840
+ for (let i = 0; i < input.length && i < embedding.length; i++) embedding[i] = input.charCodeAt(i) / 255;
8841
+ let norm = 0;
8842
+ for (let i = 0; i < embedding.length; i++) norm += embedding[i] * embedding[i];
8843
+ norm = Math.sqrt(norm) || 1;
8844
+ for (let i = 0; i < embedding.length; i++) embedding[i] /= norm;
8845
+ const results = sr.routeWithEmbedding(embedding, topK);
8846
+ spinner.stop();
8847
+ if (opts.json) { console.log(JSON.stringify({ input, results }, null, 2)); return; }
8848
+ console.log(chalk.bold.cyan(`\n Route Classification: "${input.length > 50 ? input.slice(0, 50) + '...' : input}"\n`));
8849
+ if (!results || results.length === 0) {
8850
+ console.log(chalk.yellow(' No matching routes found.'));
8851
+ if (!routesDef) { console.log(chalk.dim(' Provide a routes file with -r/--routes to define intents.')); console.log(chalk.dim(' Example: ruvector route classify "hello" -r routes.json')); }
8852
+ } else {
8853
+ for (let i = 0; i < results.length; i++) {
8854
+ const r = results[i];
8855
+ console.log(chalk.white(` ${i + 1}. ${chalk.bold(r.intent)}`));
8856
+ console.log(chalk.dim(` Score: ${r.score.toFixed(4)}`));
8857
+ if (r.metadata && Object.keys(r.metadata).length > 0) console.log(chalk.dim(` Metadata: ${JSON.stringify(r.metadata)}`));
8858
+ }
8859
+ }
8860
+ console.log('');
8861
+ } catch (err) { spinner.fail('Classification failed'); console.error(chalk.red(` ${err.message}`)); process.exit(1); }
9019
8862
  });
9020
8863
 
9021
8864
  routeCmd.command('benchmark')
9022
8865
  .description('Benchmark routing throughput')
9023
8866
  .option('-n, --iterations <n>', 'Number of iterations', '1000')
9024
- .action((opts) => {
9025
- const router = requireRouter();
9026
- const n = parseInt(opts.iterations);
9027
- const input = 'test input for routing benchmark';
9028
- const start = performance.now();
9029
- for (let i = 0; i < n; i++) {
9030
- router.classify ? router.classify(input) : null;
9031
- }
9032
- const elapsed = performance.now() - start;
9033
- console.log(chalk.bold.cyan('\nRoute Benchmark\n'));
9034
- console.log(` ${chalk.bold('Iterations:')} ${n}`);
9035
- console.log(` ${chalk.bold('Total:')} ${elapsed.toFixed(2)}ms`);
9036
- console.log(` ${chalk.bold('Per-route:')} ${(elapsed / n).toFixed(3)}ms`);
9037
- console.log(` ${chalk.bold('Throughput:')} ${Math.floor(n / (elapsed / 1000))}/sec`);
9038
- console.log();
8867
+ .option('--json', 'JSON output')
8868
+ .action(async (opts) => {
8869
+ let router;
8870
+ try { router = require('@ruvector/router'); } catch (e) {
8871
+ console.error(chalk.red('Route commands require @ruvector/router'));
8872
+ console.error(chalk.yellow(' npm install @ruvector/router'));
8873
+ console.error(chalk.dim(' or: npx ruvector install router'));
8874
+ process.exit(1);
8875
+ }
8876
+ const iterations = parseInt(opts.iterations, 10) || 1000;
8877
+ const spinner = ora(`Benchmarking ${iterations} route classifications...`).start();
8878
+ try {
8879
+ const { performance } = require('perf_hooks');
8880
+ const dim = 128;
8881
+ const sr = new router.SemanticRouter({ dimension: dim, threshold: 0.3 });
8882
+ const intentNames = ['greeting', 'farewell', 'question', 'command', 'feedback'];
8883
+ for (const name of intentNames) { const emb = new Float32Array(dim); for (let i = 0; i < dim; i++) emb[i] = Math.random() - 0.5; sr.addIntent({ name, utterances: [`example ${name}`], embedding: emb }); }
8884
+ const warmupEmb = new Float32Array(dim); for (let i = 0; i < dim; i++) warmupEmb[i] = Math.random() - 0.5;
8885
+ for (let i = 0; i < 50; i++) sr.routeWithEmbedding(warmupEmb, 3);
8886
+ const times = [], start = performance.now();
8887
+ for (let i = 0; i < iterations; i++) { const emb = new Float32Array(dim); for (let j = 0; j < dim; j++) emb[j] = Math.random() - 0.5; const t0 = performance.now(); sr.routeWithEmbedding(emb, 3); times.push(performance.now() - t0); }
8888
+ const totalMs = performance.now() - start;
8889
+ times.sort((a, b) => a - b);
8890
+ const avgMs = totalMs / iterations, p50 = times[Math.floor(iterations * 0.5)], p95 = times[Math.floor(iterations * 0.95)], p99 = times[Math.floor(iterations * 0.99)], opsPerSec = (iterations / totalMs) * 1000;
8891
+ spinner.stop();
8892
+ if (opts.json) { console.log(JSON.stringify({ iterations, dimension: dim, intents: intentNames.length, totalMs: +totalMs.toFixed(2), avgMs: +avgMs.toFixed(3), p50: +p50.toFixed(3), p95: +p95.toFixed(3), p99: +p99.toFixed(3), opsPerSec: +opsPerSec.toFixed(1) }, null, 2)); return; }
8893
+ console.log(chalk.bold.cyan('\n Router Benchmark Results\n'));
8894
+ console.log(chalk.white(` Iterations: ${iterations}`));
8895
+ console.log(chalk.white(` Dimension: ${dim}`));
8896
+ console.log(chalk.white(` Intents: ${intentNames.length}`));
8897
+ console.log(chalk.white(` Total time: ${totalMs.toFixed(1)}ms`));
8898
+ console.log(chalk.white(` Avg latency: ${avgMs.toFixed(3)}ms`));
8899
+ console.log(chalk.white(` P50: ${p50.toFixed(3)}ms`));
8900
+ console.log(chalk.white(` P95: ${p95.toFixed(3)}ms`));
8901
+ console.log(chalk.white(` P99: ${p99.toFixed(3)}ms`));
8902
+ console.log(chalk.green(` Throughput: ${opsPerSec.toFixed(1)} ops/sec`));
8903
+ console.log('');
8904
+ } catch (err) { spinner.fail('Benchmark failed'); console.error(chalk.red(` ${err.message}`)); process.exit(1); }
9039
8905
  });
9040
8906
 
9041
8907
  routeCmd.command('info')
9042
8908
  .description('Show router module information')
9043
- .action(() => {
9044
- const router = requireRouter();
9045
- console.log(chalk.bold.cyan('\nRouter Info\n'));
9046
- console.log(` ${chalk.bold('Version:')} ${typeof router.version === 'function' ? router.version() : router.version || 'unknown'}`);
9047
- console.log();
8909
+ .option('--json', 'JSON output')
8910
+ .action(async (opts) => {
8911
+ let routerAvailable = false, routerVersion = 'N/A', metrics = {};
8912
+ try { const router = require('@ruvector/router'); routerAvailable = true; routerVersion = router.version || '0.1.28'; const sr = new router.SemanticRouter({ dimension: 16 }); metrics = { distanceMetrics: Object.keys(router.DistanceMetric || {}), hasSemanticRouter: true }; } catch (e) { /* Not available */ }
8913
+ const info = { package: '@ruvector/router', available: routerAvailable, version: routerVersion, backend: 'Rust NAPI (HNSW + SIMD)', features: ['Semantic intent matching', 'HNSW indexing', 'SIMD-accelerated search', 'Multiple distance metrics', 'Save/load state', 'Async embedding support'], ...metrics };
8914
+ if (opts.json) { console.log(JSON.stringify(info, null, 2)); return; }
8915
+ console.log(chalk.bold.cyan('\n Semantic Router Information\n'));
8916
+ console.log(chalk.white(` Package: ${info.package}`));
8917
+ console.log(chalk.white(` Status: ${routerAvailable ? chalk.green('Available') + chalk.dim(` (v${routerVersion})`) : chalk.yellow('Not installed')}`));
8918
+ console.log(chalk.white(` Backend: ${info.backend}`));
8919
+ if (info.distanceMetrics) console.log(chalk.white(` Metrics: ${info.distanceMetrics.join(', ')}`));
8920
+ console.log(chalk.white('\n Features:'));
8921
+ for (const f of info.features) console.log(chalk.dim(` - ${f}`));
8922
+ if (!routerAvailable) console.log(chalk.yellow('\n Install: npm install @ruvector/router'));
8923
+ console.log('');
8924
+ });
8925
+
8926
+ // ── Decompile Command ──────────────────────────────────────────────────────
8927
+ const decompileCmd = program
8928
+ .command('decompile [target]')
8929
+ .description('Decompile npm packages, local JS files, or URLs into modules')
8930
+ .option('-o, --output <dir>', 'Output directory')
8931
+ .option('-f, --format <type>', 'Output format: modules, single, json', 'modules')
8932
+ .option('-c, --confidence <n>', 'Minimum confidence threshold (0-1)', '0.3')
8933
+ .option('--no-witness', 'Skip witness chain generation')
8934
+ .option('--json', 'JSON output to stdout (for piping)')
8935
+ .option('-q, --quiet', 'Suppress progress output')
8936
+ .option('--version-pkg <ver>', 'Package version (alternative to @version syntax)')
8937
+ .option('--diff <version>', 'Compare against another version')
8938
+ .action(async (target, opts) => {
8939
+ if (!target) {
8940
+ console.log(chalk.cyan('\nUsage:'));
8941
+ console.log(chalk.white(' ruvector decompile <package> Decompile npm package'));
8942
+ console.log(chalk.white(' ruvector decompile <pkg>@<ver> Specific version'));
8943
+ console.log(chalk.white(' ruvector decompile ./bundle.js Local file'));
8944
+ console.log(chalk.white(' ruvector decompile https://unpkg.com/x URL'));
8945
+ console.log(chalk.dim('\nOptions:'));
8946
+ console.log(chalk.dim(' -o, --output <dir> Output directory'));
8947
+ console.log(chalk.dim(' -f, --format <type> modules | single | json'));
8948
+ console.log(chalk.dim(' -c, --confidence <n> Min confidence (0-1, default: 0.3)'));
8949
+ console.log(chalk.dim(' --no-witness Skip witness chain'));
8950
+ console.log(chalk.dim(' --json JSON to stdout'));
8951
+ console.log(chalk.dim(' --diff <version> Diff against another version'));
8952
+ console.log('');
8953
+ return;
8954
+ }
8955
+
8956
+ const decompiler = require('../src/decompiler/index.js');
8957
+ const { parseTarget } = decompiler;
8958
+ const parsed = parseTarget(target);
8959
+ const minConfidence = parseFloat(opts.confidence);
8960
+ const decompileOpts = { minConfidence, witness: opts.witness !== false, useRust: true };
8961
+ const quiet = opts.quiet || opts.json;
8962
+ let spinner = null;
8963
+
8964
+ if (!quiet) {
8965
+ spinner = ora('Analyzing target...').start();
8966
+ }
8967
+
8968
+ try {
8969
+ let result;
8970
+
8971
+ if (parsed.type === 'npm') {
8972
+ const version = opts.versionPkg || parsed.version;
8973
+ if (!quiet) spinner.text = `Fetching ${parsed.name}${version ? '@' + version : ''}...`;
8974
+ result = await decompiler.decompilePackage(parsed.name, version, decompileOpts);
8975
+ if (!quiet) spinner.text = `Decompiled ${result.packageInfo.name}@${result.packageInfo.version}`;
8976
+ } else if (parsed.type === 'file') {
8977
+ if (!quiet) spinner.text = `Reading ${parsed.path}...`;
8978
+ result = decompiler.decompileFile(parsed.path, { ...decompileOpts, filePath: parsed.path });
8979
+ } else if (parsed.type === 'url') {
8980
+ if (!quiet) spinner.text = `Fetching ${parsed.url}...`;
8981
+ result = await decompiler.decompileUrl(parsed.url, decompileOpts);
8982
+ }
8983
+
8984
+ if (!quiet) spinner.succeed(chalk.green('Decompilation complete'));
8985
+
8986
+ // Handle --diff flag
8987
+ if (opts.diff && parsed.type === 'npm') {
8988
+ if (!quiet) {
8989
+ const diffSpinner = ora(`Fetching ${parsed.name}@${opts.diff} for diff...`).start();
8990
+ try {
8991
+ const other = await decompiler.decompilePackage(parsed.name, opts.diff, decompileOpts);
8992
+ diffSpinner.succeed('Diff complete');
8993
+ const resultNames = new Set(result.modules.map((m) => m.name));
8994
+ const otherNames = new Set(other.modules.map((m) => m.name));
8995
+ const added = [...resultNames].filter((n) => !otherNames.has(n));
8996
+ const removed = [...otherNames].filter((n) => !resultNames.has(n));
8997
+ const common = [...resultNames].filter((n) => otherNames.has(n));
8998
+
8999
+ console.log(chalk.bold.cyan('\n Version Diff'));
9000
+ console.log(chalk.white(` ${opts.diff} -> ${result.packageInfo.version}`));
9001
+ if (added.length) console.log(chalk.green(` Added: ${added.join(', ')}`));
9002
+ if (removed.length) console.log(chalk.red(` Removed: ${removed.join(', ')}`));
9003
+ console.log(chalk.dim(` Common: ${common.length} modules`));
9004
+ console.log('');
9005
+ } catch (err) {
9006
+ diffSpinner.fail(`Diff failed: ${err.message}`);
9007
+ }
9008
+ }
9009
+ }
9010
+
9011
+ // Output
9012
+ if (opts.json) {
9013
+ const jsonOut = {
9014
+ modules: result.modules.map((m) => ({
9015
+ name: m.name, fragments: m.fragments, confidence: m.confidence,
9016
+ contentLength: m.content.length,
9017
+ })),
9018
+ metrics: result.metrics,
9019
+ witness: result.witness ? { root: result.witness.root, chain_length: result.witness.chain.length } : null,
9020
+ packageInfo: result.packageInfo || null,
9021
+ };
9022
+ console.log(JSON.stringify(jsonOut, null, 2));
9023
+ return;
9024
+ }
9025
+
9026
+ // Determine output directory
9027
+ let outputDir = opts.output;
9028
+ if (!outputDir) {
9029
+ const baseName = result.packageInfo
9030
+ ? `${result.packageInfo.name.replace('/', '-')}@${result.packageInfo.version}`
9031
+ : path.basename(target, '.js');
9032
+ outputDir = path.join(process.cwd(), 'decompiled', baseName);
9033
+ }
9034
+
9035
+ decompiler.writeOutput(result, outputDir, opts.format);
9036
+
9037
+ console.log(chalk.bold.cyan('\n Decompilation Summary'));
9038
+ console.log(chalk.white(` Modules: ${result.modules.length}`));
9039
+ console.log(chalk.white(` Source size: ${(result.metrics.source.sizeBytes / 1024).toFixed(1)} KB`));
9040
+ console.log(chalk.white(` Functions: ${result.metrics.source.functions}`));
9041
+ console.log(chalk.white(` Classes: ${result.metrics.source.classes}`));
9042
+ if (result.witness) {
9043
+ const wRoot = result.witness.root || result.witness.chain_root || '';
9044
+ console.log(chalk.white(` Witness root: ${wRoot.slice(0, 16)}...`));
9045
+ }
9046
+ console.log(chalk.green(` Output: ${outputDir}`));
9047
+ console.log('');
9048
+
9049
+ if (result.modules.length > 0) {
9050
+ console.log(chalk.dim(' Detected modules:'));
9051
+ for (const mod of result.modules) {
9052
+ const conf = (mod.confidence * 100).toFixed(0);
9053
+ console.log(chalk.dim(` ${mod.name} (${mod.fragments} fragments, ${conf}% confidence)`));
9054
+ }
9055
+ console.log('');
9056
+ }
9057
+ } catch (err) {
9058
+ if (spinner) spinner.fail(chalk.red('Decompilation failed'));
9059
+ console.error(chalk.red(` ${err.message}`));
9060
+ process.exit(1);
9061
+ }
9048
9062
  });
9049
9063
 
9050
9064
  program.parse();
9065
+
9066
+