ruvector 0.1.21 → 0.1.23

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 ADDED
@@ -0,0 +1,858 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ // Lazy load ruvector (only when needed, not for install/help commands)
10
+ let VectorDB, getVersion, getImplementationType;
11
+ let ruvectorLoaded = false;
12
+
13
+ function loadRuvector() {
14
+ if (ruvectorLoaded) return true;
15
+ try {
16
+ const ruvector = require('../dist/index.js');
17
+ VectorDB = ruvector.VectorDB;
18
+ getVersion = ruvector.getVersion;
19
+ getImplementationType = ruvector.getImplementationType;
20
+ ruvectorLoaded = true;
21
+ return true;
22
+ } catch (e) {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function requireRuvector() {
28
+ if (!loadRuvector()) {
29
+ console.error(chalk.red('Error: Failed to load ruvector. Please run: npm run build'));
30
+ console.error(chalk.yellow('Or install the package: npm install ruvector'));
31
+ process.exit(1);
32
+ }
33
+ }
34
+
35
+ // Import GNN (optional - graceful fallback if not available)
36
+ let RuvectorLayer, TensorCompress, differentiableSearch, getCompressionLevel, hierarchicalForward;
37
+ let gnnAvailable = false;
38
+ try {
39
+ const gnn = require('@ruvector/gnn');
40
+ RuvectorLayer = gnn.RuvectorLayer;
41
+ TensorCompress = gnn.TensorCompress;
42
+ differentiableSearch = gnn.differentiableSearch;
43
+ getCompressionLevel = gnn.getCompressionLevel;
44
+ hierarchicalForward = gnn.hierarchicalForward;
45
+ gnnAvailable = true;
46
+ } catch (e) {
47
+ // GNN not available - commands will show helpful message
48
+ }
49
+
50
+ const program = new Command();
51
+
52
+ // Get package version from package.json
53
+ const packageJson = require('../package.json');
54
+
55
+ // Version and description (lazy load implementation info)
56
+ program
57
+ .name('ruvector')
58
+ .description(`${chalk.cyan('ruvector')} - High-performance vector database CLI`)
59
+ .version(packageJson.version);
60
+
61
+ // Create database
62
+ program
63
+ .command('create <path>')
64
+ .description('Create a new vector database')
65
+ .option('-d, --dimension <number>', 'Vector dimension', '384')
66
+ .option('-m, --metric <type>', 'Distance metric (cosine|euclidean|dot)', 'cosine')
67
+ .action((dbPath, options) => {
68
+ requireRuvector();
69
+ const spinner = ora('Creating database...').start();
70
+
71
+ try {
72
+ const dimension = parseInt(options.dimension);
73
+ const db = new VectorDB({
74
+ dimension,
75
+ metric: options.metric,
76
+ path: dbPath,
77
+ autoPersist: true
78
+ });
79
+
80
+ db.save(dbPath);
81
+ spinner.succeed(chalk.green(`Database created: ${dbPath}`));
82
+ console.log(chalk.gray(` Dimension: ${dimension}`));
83
+ console.log(chalk.gray(` Metric: ${options.metric}`));
84
+ console.log(chalk.gray(` Implementation: ${getImplementationType()}`));
85
+ } catch (error) {
86
+ spinner.fail(chalk.red('Failed to create database'));
87
+ console.error(chalk.red(error.message));
88
+ process.exit(1);
89
+ }
90
+ });
91
+
92
+ // Insert vectors
93
+ program
94
+ .command('insert <database> <file>')
95
+ .description('Insert vectors from JSON file')
96
+ .option('-b, --batch-size <number>', 'Batch size for insertion', '1000')
97
+ .action((dbPath, file, options) => {
98
+ requireRuvector();
99
+ const spinner = ora('Loading database...').start();
100
+
101
+ try {
102
+ // Read database metadata to get dimension
103
+ let dimension = 384; // default
104
+ if (fs.existsSync(dbPath)) {
105
+ const dbData = fs.readFileSync(dbPath, 'utf8');
106
+ const parsed = JSON.parse(dbData);
107
+ dimension = parsed.dimension || 384;
108
+ }
109
+
110
+ const db = new VectorDB({ dimension });
111
+
112
+ if (fs.existsSync(dbPath)) {
113
+ db.load(dbPath);
114
+ }
115
+
116
+ spinner.text = 'Reading vectors...';
117
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
118
+ const vectors = Array.isArray(data) ? data : [data];
119
+
120
+ spinner.text = `Inserting ${vectors.length} vectors...`;
121
+ const batchSize = parseInt(options.batchSize);
122
+
123
+ for (let i = 0; i < vectors.length; i += batchSize) {
124
+ const batch = vectors.slice(i, i + batchSize);
125
+ db.insertBatch(batch);
126
+ spinner.text = `Inserted ${Math.min(i + batchSize, vectors.length)}/${vectors.length} vectors...`;
127
+ }
128
+
129
+ db.save(dbPath);
130
+ spinner.succeed(chalk.green(`Inserted ${vectors.length} vectors`));
131
+
132
+ const stats = db.stats();
133
+ console.log(chalk.gray(` Total vectors: ${stats.count}`));
134
+ } catch (error) {
135
+ spinner.fail(chalk.red('Failed to insert vectors'));
136
+ console.error(chalk.red(error.message));
137
+ process.exit(1);
138
+ }
139
+ });
140
+
141
+ // Search vectors
142
+ program
143
+ .command('search <database>')
144
+ .description('Search for similar vectors')
145
+ .requiredOption('-v, --vector <json>', 'Query vector as JSON array')
146
+ .option('-k, --top-k <number>', 'Number of results', '10')
147
+ .option('-t, --threshold <number>', 'Similarity threshold', '0.0')
148
+ .option('-f, --filter <json>', 'Metadata filter as JSON')
149
+ .action((dbPath, options) => {
150
+ requireRuvector();
151
+ const spinner = ora('Loading database...').start();
152
+
153
+ try {
154
+ // Read database metadata
155
+ const dbData = fs.readFileSync(dbPath, 'utf8');
156
+ const parsed = JSON.parse(dbData);
157
+ const dimension = parsed.dimension || 384;
158
+
159
+ const db = new VectorDB({ dimension });
160
+ db.load(dbPath);
161
+
162
+ spinner.text = 'Searching...';
163
+
164
+ const vector = JSON.parse(options.vector);
165
+ const query = {
166
+ vector,
167
+ k: parseInt(options.topK),
168
+ threshold: parseFloat(options.threshold)
169
+ };
170
+
171
+ if (options.filter) {
172
+ query.filter = JSON.parse(options.filter);
173
+ }
174
+
175
+ const results = db.search(query);
176
+ spinner.succeed(chalk.green(`Found ${results.length} results`));
177
+
178
+ console.log(chalk.cyan('\nSearch Results:'));
179
+ results.forEach((result, i) => {
180
+ console.log(chalk.white(`\n${i + 1}. ID: ${result.id}`));
181
+ console.log(chalk.yellow(` Score: ${result.score.toFixed(4)}`));
182
+ if (result.metadata) {
183
+ console.log(chalk.gray(` Metadata: ${JSON.stringify(result.metadata)}`));
184
+ }
185
+ });
186
+ } catch (error) {
187
+ spinner.fail(chalk.red('Failed to search'));
188
+ console.error(chalk.red(error.message));
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ // Show stats
194
+ program
195
+ .command('stats <database>')
196
+ .description('Show database statistics')
197
+ .action((dbPath) => {
198
+ requireRuvector();
199
+ const spinner = ora('Loading database...').start();
200
+
201
+ try {
202
+ const dbData = fs.readFileSync(dbPath, 'utf8');
203
+ const parsed = JSON.parse(dbData);
204
+ const dimension = parsed.dimension || 384;
205
+
206
+ const db = new VectorDB({ dimension });
207
+ db.load(dbPath);
208
+
209
+ const stats = db.stats();
210
+ spinner.succeed(chalk.green('Database statistics'));
211
+
212
+ console.log(chalk.cyan('\nDatabase Stats:'));
213
+ console.log(chalk.white(` Vector Count: ${chalk.yellow(stats.count)}`));
214
+ console.log(chalk.white(` Dimension: ${chalk.yellow(stats.dimension)}`));
215
+ console.log(chalk.white(` Metric: ${chalk.yellow(stats.metric)}`));
216
+ console.log(chalk.white(` Implementation: ${chalk.yellow(getImplementationType())}`));
217
+
218
+ if (stats.memoryUsage) {
219
+ const mb = (stats.memoryUsage / (1024 * 1024)).toFixed(2);
220
+ console.log(chalk.white(` Memory Usage: ${chalk.yellow(mb + ' MB')}`));
221
+ }
222
+
223
+ const fileStats = fs.statSync(dbPath);
224
+ const fileMb = (fileStats.size / (1024 * 1024)).toFixed(2);
225
+ console.log(chalk.white(` File Size: ${chalk.yellow(fileMb + ' MB')}`));
226
+ } catch (error) {
227
+ spinner.fail(chalk.red('Failed to load database'));
228
+ console.error(chalk.red(error.message));
229
+ process.exit(1);
230
+ }
231
+ });
232
+
233
+ // Benchmark
234
+ program
235
+ .command('benchmark')
236
+ .description('Run performance benchmarks')
237
+ .option('-d, --dimension <number>', 'Vector dimension', '384')
238
+ .option('-n, --num-vectors <number>', 'Number of vectors', '10000')
239
+ .option('-q, --num-queries <number>', 'Number of queries', '1000')
240
+ .action((options) => {
241
+ requireRuvector();
242
+ console.log(chalk.cyan('\nruvector Performance Benchmark'));
243
+ console.log(chalk.gray(`Implementation: ${getImplementationType()}\n`));
244
+
245
+ const dimension = parseInt(options.dimension);
246
+ const numVectors = parseInt(options.numVectors);
247
+ const numQueries = parseInt(options.numQueries);
248
+
249
+ let spinner = ora('Creating database...').start();
250
+
251
+ try {
252
+ const db = new VectorDB({ dimension, metric: 'cosine' });
253
+ spinner.succeed();
254
+
255
+ // Insert benchmark
256
+ spinner = ora(`Inserting ${numVectors} vectors...`).start();
257
+ const insertStart = Date.now();
258
+
259
+ const vectors = [];
260
+ for (let i = 0; i < numVectors; i++) {
261
+ vectors.push({
262
+ id: `vec_${i}`,
263
+ vector: Array.from({ length: dimension }, () => Math.random()),
264
+ metadata: { index: i, batch: Math.floor(i / 1000) }
265
+ });
266
+ }
267
+
268
+ db.insertBatch(vectors);
269
+ const insertTime = Date.now() - insertStart;
270
+ const insertRate = (numVectors / (insertTime / 1000)).toFixed(0);
271
+
272
+ spinner.succeed(chalk.green(`Inserted ${numVectors} vectors in ${insertTime}ms`));
273
+ console.log(chalk.gray(` Rate: ${chalk.yellow(insertRate)} vectors/sec`));
274
+
275
+ // Search benchmark
276
+ spinner = ora(`Running ${numQueries} searches...`).start();
277
+ const searchStart = Date.now();
278
+
279
+ for (let i = 0; i < numQueries; i++) {
280
+ const query = {
281
+ vector: Array.from({ length: dimension }, () => Math.random()),
282
+ k: 10
283
+ };
284
+ db.search(query);
285
+ }
286
+
287
+ const searchTime = Date.now() - searchStart;
288
+ const searchRate = (numQueries / (searchTime / 1000)).toFixed(0);
289
+ const avgLatency = (searchTime / numQueries).toFixed(2);
290
+
291
+ spinner.succeed(chalk.green(`Completed ${numQueries} searches in ${searchTime}ms`));
292
+ console.log(chalk.gray(` Rate: ${chalk.yellow(searchRate)} queries/sec`));
293
+ console.log(chalk.gray(` Avg Latency: ${chalk.yellow(avgLatency)}ms`));
294
+
295
+ // Stats
296
+ const stats = db.stats();
297
+ console.log(chalk.cyan('\nFinal Stats:'));
298
+ console.log(chalk.white(` Vector Count: ${chalk.yellow(stats.count)}`));
299
+ console.log(chalk.white(` Dimension: ${chalk.yellow(stats.dimension)}`));
300
+ console.log(chalk.white(` Implementation: ${chalk.yellow(getImplementationType())}`));
301
+
302
+ } catch (error) {
303
+ spinner.fail(chalk.red('Benchmark failed'));
304
+ console.error(chalk.red(error.message));
305
+ process.exit(1);
306
+ }
307
+ });
308
+
309
+ // Info command
310
+ program
311
+ .command('info')
312
+ .description('Show ruvector information')
313
+ .action(() => {
314
+ console.log(chalk.cyan('\nruvector Information'));
315
+ console.log(chalk.white(` CLI Version: ${chalk.yellow(packageJson.version)}`));
316
+
317
+ // Try to load ruvector for implementation info
318
+ if (loadRuvector()) {
319
+ const info = getVersion();
320
+ console.log(chalk.white(` Core Version: ${chalk.yellow(info.version)}`));
321
+ console.log(chalk.white(` Implementation: ${chalk.yellow(info.implementation)}`));
322
+ } else {
323
+ console.log(chalk.white(` Core: ${chalk.gray('Not loaded (install @ruvector/core)')}`));
324
+ }
325
+
326
+ console.log(chalk.white(` GNN Module: ${gnnAvailable ? chalk.green('Available') : chalk.gray('Not installed')}`));
327
+ console.log(chalk.white(` Node Version: ${chalk.yellow(process.version)}`));
328
+ console.log(chalk.white(` Platform: ${chalk.yellow(process.platform)}`));
329
+ console.log(chalk.white(` Architecture: ${chalk.yellow(process.arch)}`));
330
+
331
+ if (!gnnAvailable) {
332
+ console.log(chalk.gray('\n Install GNN with: npx ruvector install gnn'));
333
+ }
334
+ });
335
+
336
+ // =============================================================================
337
+ // Install Command
338
+ // =============================================================================
339
+
340
+ program
341
+ .command('install [packages...]')
342
+ .description('Install optional ruvector packages')
343
+ .option('-a, --all', 'Install all optional packages')
344
+ .option('-l, --list', 'List available packages')
345
+ .option('-i, --interactive', 'Interactive package selection')
346
+ .action(async (packages, options) => {
347
+ const { execSync } = require('child_process');
348
+
349
+ // Available optional packages - all ruvector npm packages
350
+ const availablePackages = {
351
+ // Core packages
352
+ core: {
353
+ name: '@ruvector/core',
354
+ description: 'Core vector database with native Rust bindings (HNSW, SIMD)',
355
+ installed: true, // Always installed with ruvector
356
+ category: 'core'
357
+ },
358
+ gnn: {
359
+ name: '@ruvector/gnn',
360
+ description: 'Graph Neural Network layers, tensor compression, differentiable search',
361
+ installed: gnnAvailable,
362
+ category: 'core'
363
+ },
364
+ 'graph-node': {
365
+ name: '@ruvector/graph-node',
366
+ description: 'Native Node.js bindings for hypergraph database with Cypher queries',
367
+ installed: false,
368
+ category: 'core'
369
+ },
370
+ 'agentic-synth': {
371
+ name: '@ruvector/agentic-synth',
372
+ description: 'Synthetic data generator for AI/ML training, RAG, and agentic workflows',
373
+ installed: false,
374
+ category: 'tools'
375
+ },
376
+ extensions: {
377
+ name: 'ruvector-extensions',
378
+ description: 'Advanced features: embeddings, UI, exports, temporal tracking, persistence',
379
+ installed: false,
380
+ category: 'tools'
381
+ },
382
+ // Platform-specific native bindings for @ruvector/core
383
+ 'node-linux-x64': {
384
+ name: '@ruvector/node-linux-x64-gnu',
385
+ description: 'Linux x64 native bindings for @ruvector/core',
386
+ installed: false,
387
+ category: 'platform'
388
+ },
389
+ 'node-linux-arm64': {
390
+ name: '@ruvector/node-linux-arm64-gnu',
391
+ description: 'Linux ARM64 native bindings for @ruvector/core',
392
+ installed: false,
393
+ category: 'platform'
394
+ },
395
+ 'node-darwin-x64': {
396
+ name: '@ruvector/node-darwin-x64',
397
+ description: 'macOS Intel x64 native bindings for @ruvector/core',
398
+ installed: false,
399
+ category: 'platform'
400
+ },
401
+ 'node-darwin-arm64': {
402
+ name: '@ruvector/node-darwin-arm64',
403
+ description: 'macOS Apple Silicon native bindings for @ruvector/core',
404
+ installed: false,
405
+ category: 'platform'
406
+ },
407
+ 'node-win32-x64': {
408
+ name: '@ruvector/node-win32-x64-msvc',
409
+ description: 'Windows x64 native bindings for @ruvector/core',
410
+ installed: false,
411
+ category: 'platform'
412
+ },
413
+ // Platform-specific native bindings for @ruvector/gnn
414
+ 'gnn-linux-x64': {
415
+ name: '@ruvector/gnn-linux-x64-gnu',
416
+ description: 'Linux x64 native bindings for @ruvector/gnn',
417
+ installed: false,
418
+ category: 'platform'
419
+ },
420
+ 'gnn-linux-arm64': {
421
+ name: '@ruvector/gnn-linux-arm64-gnu',
422
+ description: 'Linux ARM64 native bindings for @ruvector/gnn',
423
+ installed: false,
424
+ category: 'platform'
425
+ },
426
+ 'gnn-darwin-x64': {
427
+ name: '@ruvector/gnn-darwin-x64',
428
+ description: 'macOS Intel x64 native bindings for @ruvector/gnn',
429
+ installed: false,
430
+ category: 'platform'
431
+ },
432
+ 'gnn-darwin-arm64': {
433
+ name: '@ruvector/gnn-darwin-arm64',
434
+ description: 'macOS Apple Silicon native bindings for @ruvector/gnn',
435
+ installed: false,
436
+ category: 'platform'
437
+ },
438
+ 'gnn-win32-x64': {
439
+ name: '@ruvector/gnn-win32-x64-msvc',
440
+ description: 'Windows x64 native bindings for @ruvector/gnn',
441
+ installed: false,
442
+ category: 'platform'
443
+ },
444
+ // Legacy/standalone packages
445
+ 'ruvector-core': {
446
+ name: 'ruvector-core',
447
+ description: 'Standalone vector database (legacy, use @ruvector/core instead)',
448
+ installed: false,
449
+ category: 'legacy'
450
+ }
451
+ };
452
+
453
+ // Check which packages are actually installed
454
+ for (const [key, pkg] of Object.entries(availablePackages)) {
455
+ if (key !== 'core' && key !== 'gnn') {
456
+ try {
457
+ require.resolve(pkg.name);
458
+ pkg.installed = true;
459
+ } catch (e) {
460
+ pkg.installed = false;
461
+ }
462
+ }
463
+ }
464
+
465
+ // List packages
466
+ if (options.list || (packages.length === 0 && !options.all && !options.interactive)) {
467
+ console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════════'));
468
+ console.log(chalk.cyan(' Ruvector Packages'));
469
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════\n'));
470
+
471
+ const categories = {
472
+ core: { title: '📦 Core Packages', packages: [] },
473
+ tools: { title: '🔧 Tools & Extensions', packages: [] },
474
+ platform: { title: '🖥️ Platform Bindings', packages: [] },
475
+ legacy: { title: '📜 Legacy Packages', packages: [] }
476
+ };
477
+
478
+ // Group by category
479
+ Object.entries(availablePackages).forEach(([key, pkg]) => {
480
+ if (categories[pkg.category]) {
481
+ categories[pkg.category].packages.push({ key, ...pkg });
482
+ }
483
+ });
484
+
485
+ // Display by category
486
+ for (const [catKey, cat] of Object.entries(categories)) {
487
+ if (cat.packages.length === 0) continue;
488
+
489
+ console.log(chalk.cyan(`${cat.title}`));
490
+ console.log(chalk.gray('─'.repeat(60)));
491
+
492
+ cat.packages.forEach(pkg => {
493
+ const status = pkg.installed ? chalk.green('✓') : chalk.gray('○');
494
+ const statusText = pkg.installed ? chalk.green('installed') : chalk.gray('available');
495
+ console.log(chalk.white(` ${status} ${chalk.yellow(pkg.key.padEnd(18))} ${statusText}`));
496
+ console.log(chalk.gray(` ${pkg.description}`));
497
+ console.log(chalk.gray(` npm: ${chalk.white(pkg.name)}\n`));
498
+ });
499
+ }
500
+
501
+ console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'));
502
+ console.log(chalk.cyan('Usage:'));
503
+ console.log(chalk.white(' npx ruvector install gnn # Install GNN package'));
504
+ console.log(chalk.white(' npx ruvector install graph-node # Install graph database'));
505
+ console.log(chalk.white(' npx ruvector install agentic-synth # Install data generator'));
506
+ console.log(chalk.white(' npx ruvector install --all # Install all core packages'));
507
+ console.log(chalk.white(' npx ruvector install -i # Interactive selection'));
508
+ console.log(chalk.gray('\n Note: Platform bindings are auto-detected by @ruvector/core'));
509
+ return;
510
+ }
511
+
512
+ // Interactive mode
513
+ if (options.interactive) {
514
+ const readline = require('readline');
515
+ const rl = readline.createInterface({
516
+ input: process.stdin,
517
+ output: process.stdout
518
+ });
519
+
520
+ console.log(chalk.cyan('\nSelect packages to install:\n'));
521
+
522
+ const notInstalled = Object.entries(availablePackages)
523
+ .filter(([_, pkg]) => !pkg.installed);
524
+
525
+ if (notInstalled.length === 0) {
526
+ console.log(chalk.green('All packages are already installed!'));
527
+ rl.close();
528
+ return;
529
+ }
530
+
531
+ notInstalled.forEach(([key, pkg], i) => {
532
+ console.log(chalk.white(` ${i + 1}. ${chalk.yellow(key)} - ${pkg.description}`));
533
+ });
534
+ console.log(chalk.white(` ${notInstalled.length + 1}. ${chalk.yellow('all')} - Install all packages`));
535
+ console.log(chalk.white(` 0. ${chalk.gray('cancel')} - Exit without installing`));
536
+
537
+ rl.question(chalk.cyan('\nEnter selection (comma-separated for multiple): '), (answer) => {
538
+ rl.close();
539
+
540
+ const selections = answer.split(',').map(s => s.trim());
541
+ let toInstall = [];
542
+
543
+ for (const sel of selections) {
544
+ if (sel === '0' || sel.toLowerCase() === 'cancel') {
545
+ console.log(chalk.yellow('Installation cancelled.'));
546
+ return;
547
+ }
548
+ if (sel === String(notInstalled.length + 1) || sel.toLowerCase() === 'all') {
549
+ toInstall = notInstalled.map(([_, pkg]) => pkg.name);
550
+ break;
551
+ }
552
+ const idx = parseInt(sel) - 1;
553
+ if (idx >= 0 && idx < notInstalled.length) {
554
+ toInstall.push(notInstalled[idx][1].name);
555
+ }
556
+ }
557
+
558
+ if (toInstall.length === 0) {
559
+ console.log(chalk.yellow('No valid packages selected.'));
560
+ return;
561
+ }
562
+
563
+ installPackages(toInstall);
564
+ });
565
+ return;
566
+ }
567
+
568
+ // Install all (core + tools only, not platform-specific or legacy)
569
+ if (options.all) {
570
+ const toInstall = Object.values(availablePackages)
571
+ .filter(pkg => !pkg.installed && (pkg.category === 'core' || pkg.category === 'tools'))
572
+ .map(pkg => pkg.name);
573
+
574
+ if (toInstall.length === 0) {
575
+ console.log(chalk.green('All core packages are already installed!'));
576
+ return;
577
+ }
578
+
579
+ console.log(chalk.cyan(`Installing ${toInstall.length} packages...`));
580
+ installPackages(toInstall);
581
+ return;
582
+ }
583
+
584
+ // Install specific packages
585
+ const toInstall = [];
586
+ for (const pkg of packages) {
587
+ const key = pkg.toLowerCase().replace('@ruvector/', '');
588
+ if (availablePackages[key]) {
589
+ if (availablePackages[key].installed) {
590
+ console.log(chalk.yellow(`${availablePackages[key].name} is already installed`));
591
+ } else {
592
+ toInstall.push(availablePackages[key].name);
593
+ }
594
+ } else {
595
+ console.log(chalk.red(`Unknown package: ${pkg}`));
596
+ console.log(chalk.gray(`Available: ${Object.keys(availablePackages).join(', ')}`));
597
+ }
598
+ }
599
+
600
+ if (toInstall.length > 0) {
601
+ installPackages(toInstall);
602
+ }
603
+
604
+ function installPackages(pkgs) {
605
+ const spinner = ora(`Installing ${pkgs.join(', ')}...`).start();
606
+
607
+ try {
608
+ // Detect package manager
609
+ let pm = 'npm';
610
+ if (fs.existsSync('yarn.lock')) pm = 'yarn';
611
+ else if (fs.existsSync('pnpm-lock.yaml')) pm = 'pnpm';
612
+ else if (fs.existsSync('bun.lockb')) pm = 'bun';
613
+
614
+ const cmd = pm === 'yarn' ? `yarn add ${pkgs.join(' ')}`
615
+ : pm === 'pnpm' ? `pnpm add ${pkgs.join(' ')}`
616
+ : pm === 'bun' ? `bun add ${pkgs.join(' ')}`
617
+ : `npm install ${pkgs.join(' ')}`;
618
+
619
+ execSync(cmd, { stdio: 'pipe' });
620
+
621
+ spinner.succeed(chalk.green(`Installed: ${pkgs.join(', ')}`));
622
+ console.log(chalk.cyan('\nRun "npx ruvector info" to verify installation.'));
623
+ } catch (error) {
624
+ spinner.fail(chalk.red('Installation failed'));
625
+ console.error(chalk.red(error.message));
626
+ console.log(chalk.yellow(`\nTry manually: npm install ${pkgs.join(' ')}`));
627
+ process.exit(1);
628
+ }
629
+ }
630
+ });
631
+
632
+ // =============================================================================
633
+ // GNN Commands
634
+ // =============================================================================
635
+
636
+ // Helper to check GNN availability
637
+ function requireGnn() {
638
+ if (!gnnAvailable) {
639
+ console.error(chalk.red('Error: GNN module not available.'));
640
+ console.error(chalk.yellow('Install it with: npm install @ruvector/gnn'));
641
+ process.exit(1);
642
+ }
643
+ }
644
+
645
+ // GNN parent command
646
+ const gnnCmd = program
647
+ .command('gnn')
648
+ .description('Graph Neural Network operations');
649
+
650
+ // GNN Layer command
651
+ gnnCmd
652
+ .command('layer')
653
+ .description('Create and test a GNN layer')
654
+ .requiredOption('-i, --input-dim <number>', 'Input dimension')
655
+ .requiredOption('-h, --hidden-dim <number>', 'Hidden dimension')
656
+ .option('-a, --heads <number>', 'Number of attention heads', '4')
657
+ .option('-d, --dropout <number>', 'Dropout rate', '0.1')
658
+ .option('--test', 'Run a test forward pass')
659
+ .option('-o, --output <file>', 'Save layer config to JSON file')
660
+ .action((options) => {
661
+ requireGnn();
662
+ const spinner = ora('Creating GNN layer...').start();
663
+
664
+ try {
665
+ const inputDim = parseInt(options.inputDim);
666
+ const hiddenDim = parseInt(options.hiddenDim);
667
+ const heads = parseInt(options.heads);
668
+ const dropout = parseFloat(options.dropout);
669
+
670
+ const layer = new RuvectorLayer(inputDim, hiddenDim, heads, dropout);
671
+ spinner.succeed(chalk.green('GNN Layer created'));
672
+
673
+ console.log(chalk.cyan('\nLayer Configuration:'));
674
+ console.log(chalk.white(` Input Dim: ${chalk.yellow(inputDim)}`));
675
+ console.log(chalk.white(` Hidden Dim: ${chalk.yellow(hiddenDim)}`));
676
+ console.log(chalk.white(` Heads: ${chalk.yellow(heads)}`));
677
+ console.log(chalk.white(` Dropout: ${chalk.yellow(dropout)}`));
678
+
679
+ if (options.test) {
680
+ spinner.start('Running test forward pass...');
681
+
682
+ // Create test data
683
+ const nodeEmbedding = Array.from({ length: inputDim }, () => Math.random());
684
+ const neighborEmbeddings = [
685
+ Array.from({ length: inputDim }, () => Math.random()),
686
+ Array.from({ length: inputDim }, () => Math.random())
687
+ ];
688
+ const edgeWeights = [0.6, 0.4];
689
+
690
+ const output = layer.forward(nodeEmbedding, neighborEmbeddings, edgeWeights);
691
+ spinner.succeed(chalk.green('Forward pass completed'));
692
+
693
+ console.log(chalk.cyan('\nTest Results:'));
694
+ console.log(chalk.white(` Input shape: ${chalk.yellow(`[${inputDim}]`)}`));
695
+ console.log(chalk.white(` Output shape: ${chalk.yellow(`[${output.length}]`)}`));
696
+ console.log(chalk.white(` Output sample: ${chalk.gray(`[${output.slice(0, 4).map(v => v.toFixed(4)).join(', ')}...]`)}`));
697
+ }
698
+
699
+ if (options.output) {
700
+ const config = layer.toJson();
701
+ fs.writeFileSync(options.output, config);
702
+ console.log(chalk.green(`\nLayer config saved to: ${options.output}`));
703
+ }
704
+ } catch (error) {
705
+ spinner.fail(chalk.red('Failed to create GNN layer'));
706
+ console.error(chalk.red(error.message));
707
+ process.exit(1);
708
+ }
709
+ });
710
+
711
+ // GNN Compress command
712
+ gnnCmd
713
+ .command('compress')
714
+ .description('Compress embeddings using adaptive tensor compression')
715
+ .requiredOption('-f, --file <path>', 'Input JSON file with embeddings')
716
+ .option('-l, --level <type>', 'Compression level (none|half|pq8|pq4|binary)', 'auto')
717
+ .option('-a, --access-freq <number>', 'Access frequency for auto compression (0.0-1.0)', '0.5')
718
+ .option('-o, --output <file>', 'Output file for compressed data')
719
+ .action((options) => {
720
+ requireGnn();
721
+ const spinner = ora('Loading embeddings...').start();
722
+
723
+ try {
724
+ const data = JSON.parse(fs.readFileSync(options.file, 'utf8'));
725
+ const embeddings = Array.isArray(data) ? data : [data];
726
+
727
+ spinner.text = 'Compressing embeddings...';
728
+ const compressor = new TensorCompress();
729
+ const accessFreq = parseFloat(options.accessFreq);
730
+
731
+ const results = [];
732
+ let totalOriginalSize = 0;
733
+ let totalCompressedSize = 0;
734
+
735
+ for (const embedding of embeddings) {
736
+ const vec = embedding.vector || embedding;
737
+ totalOriginalSize += vec.length * 4; // float32 = 4 bytes
738
+
739
+ let compressed;
740
+ if (options.level === 'auto') {
741
+ compressed = compressor.compress(vec, accessFreq);
742
+ } else {
743
+ const levelConfig = { levelType: options.level };
744
+ if (options.level === 'pq8') {
745
+ levelConfig.subvectors = 8;
746
+ levelConfig.centroids = 256;
747
+ } else if (options.level === 'pq4') {
748
+ levelConfig.subvectors = 8;
749
+ }
750
+ compressed = compressor.compressWithLevel(vec, levelConfig);
751
+ }
752
+
753
+ totalCompressedSize += compressed.length;
754
+ results.push({
755
+ id: embedding.id,
756
+ compressed
757
+ });
758
+ }
759
+
760
+ const ratio = (totalOriginalSize / totalCompressedSize).toFixed(2);
761
+ const savings = ((1 - totalCompressedSize / totalOriginalSize) * 100).toFixed(1);
762
+
763
+ spinner.succeed(chalk.green(`Compressed ${embeddings.length} embeddings`));
764
+
765
+ console.log(chalk.cyan('\nCompression Results:'));
766
+ console.log(chalk.white(` Embeddings: ${chalk.yellow(embeddings.length)}`));
767
+ console.log(chalk.white(` Level: ${chalk.yellow(options.level === 'auto' ? `auto (${getCompressionLevel(accessFreq)})` : options.level)}`));
768
+ console.log(chalk.white(` Original: ${chalk.yellow((totalOriginalSize / 1024).toFixed(2) + ' KB')}`));
769
+ console.log(chalk.white(` Compressed: ${chalk.yellow((totalCompressedSize / 1024).toFixed(2) + ' KB')}`));
770
+ console.log(chalk.white(` Ratio: ${chalk.yellow(ratio + 'x')}`));
771
+ console.log(chalk.white(` Savings: ${chalk.yellow(savings + '%')}`));
772
+
773
+ if (options.output) {
774
+ fs.writeFileSync(options.output, JSON.stringify(results, null, 2));
775
+ console.log(chalk.green(`\nCompressed data saved to: ${options.output}`));
776
+ }
777
+ } catch (error) {
778
+ spinner.fail(chalk.red('Failed to compress embeddings'));
779
+ console.error(chalk.red(error.message));
780
+ process.exit(1);
781
+ }
782
+ });
783
+
784
+ // GNN Search command
785
+ gnnCmd
786
+ .command('search')
787
+ .description('Differentiable search with soft attention')
788
+ .requiredOption('-q, --query <json>', 'Query vector as JSON array')
789
+ .requiredOption('-c, --candidates <file>', 'Candidates file (JSON array of vectors)')
790
+ .option('-k, --top-k <number>', 'Number of results', '5')
791
+ .option('-t, --temperature <number>', 'Softmax temperature (lower=sharper)', '1.0')
792
+ .action((options) => {
793
+ requireGnn();
794
+ const spinner = ora('Loading candidates...').start();
795
+
796
+ try {
797
+ const query = JSON.parse(options.query);
798
+ const candidatesData = JSON.parse(fs.readFileSync(options.candidates, 'utf8'));
799
+ const candidates = candidatesData.map(c => c.vector || c);
800
+ const k = parseInt(options.topK);
801
+ const temperature = parseFloat(options.temperature);
802
+
803
+ spinner.text = 'Running differentiable search...';
804
+ const result = differentiableSearch(query, candidates, k, temperature);
805
+
806
+ spinner.succeed(chalk.green(`Found top-${k} results`));
807
+
808
+ console.log(chalk.cyan('\nSearch Results:'));
809
+ console.log(chalk.white(` Query dim: ${chalk.yellow(query.length)}`));
810
+ console.log(chalk.white(` Candidates: ${chalk.yellow(candidates.length)}`));
811
+ console.log(chalk.white(` Temperature: ${chalk.yellow(temperature)}`));
812
+
813
+ console.log(chalk.cyan('\nTop-K Results:'));
814
+ for (let i = 0; i < result.indices.length; i++) {
815
+ const idx = result.indices[i];
816
+ const weight = result.weights[i];
817
+ const id = candidatesData[idx]?.id || `candidate_${idx}`;
818
+ console.log(chalk.white(` ${i + 1}. ${chalk.yellow(id)} (index: ${idx})`));
819
+ console.log(chalk.gray(` Weight: ${weight.toFixed(6)}`));
820
+ }
821
+ } catch (error) {
822
+ spinner.fail(chalk.red('Failed to run search'));
823
+ console.error(chalk.red(error.message));
824
+ process.exit(1);
825
+ }
826
+ });
827
+
828
+ // GNN Info command
829
+ gnnCmd
830
+ .command('info')
831
+ .description('Show GNN module information')
832
+ .action(() => {
833
+ if (!gnnAvailable) {
834
+ console.log(chalk.yellow('\nGNN Module: Not installed'));
835
+ console.log(chalk.white('Install with: npm install @ruvector/gnn'));
836
+ return;
837
+ }
838
+
839
+ console.log(chalk.cyan('\nGNN Module Information'));
840
+ console.log(chalk.white(` Status: ${chalk.green('Available')}`));
841
+ console.log(chalk.white(` Platform: ${chalk.yellow(process.platform)}`));
842
+ console.log(chalk.white(` Architecture: ${chalk.yellow(process.arch)}`));
843
+
844
+ console.log(chalk.cyan('\nAvailable Features:'));
845
+ console.log(chalk.white(` • RuvectorLayer - GNN layer with multi-head attention`));
846
+ console.log(chalk.white(` • TensorCompress - Adaptive tensor compression (5 levels)`));
847
+ console.log(chalk.white(` • differentiableSearch - Soft attention-based search`));
848
+ console.log(chalk.white(` • hierarchicalForward - Multi-layer GNN processing`));
849
+
850
+ console.log(chalk.cyan('\nCompression Levels:'));
851
+ console.log(chalk.gray(` none (freq > 0.8) - Full precision, hot data`));
852
+ console.log(chalk.gray(` half (freq > 0.4) - ~50% savings, warm data`));
853
+ console.log(chalk.gray(` pq8 (freq > 0.1) - ~8x compression, cool data`));
854
+ console.log(chalk.gray(` pq4 (freq > 0.01) - ~16x compression, cold data`));
855
+ console.log(chalk.gray(` binary (freq <= 0.01) - ~32x compression, archive`));
856
+ });
857
+
858
+ program.parse();