@ruvector/edge-net 0.5.0 → 0.5.3

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.
@@ -0,0 +1,1008 @@
1
+ /**
2
+ * AdapterHub - Community Adapter Registry and Management
3
+ *
4
+ * Provides a marketplace-style interface for browsing, uploading,
5
+ * downloading, and applying community-created LoRA adapters.
6
+ *
7
+ * @module @ruvector/edge-net/models/adapter-hub
8
+ *
9
+ * @example
10
+ * ```javascript
11
+ * import { AdapterHub } from '@ruvector/edge-net/models';
12
+ *
13
+ * const hub = new AdapterHub();
14
+ *
15
+ * // Browse adapters by category
16
+ * const codeAdapters = await hub.browse({ domain: 'code', sort: 'rating' });
17
+ *
18
+ * // Download and apply an adapter
19
+ * const adapter = await hub.download('popular-code-adapter-v1');
20
+ * await myLoRA.loadAdapter(adapter);
21
+ *
22
+ * // Upload your own adapter
23
+ * await hub.upload(myLoRA, {
24
+ * name: 'My Code Assistant',
25
+ * description: 'Fine-tuned for Python coding',
26
+ * domain: 'code',
27
+ * tags: ['python', 'coding', 'assistant']
28
+ * });
29
+ * ```
30
+ */
31
+
32
+ import { EventEmitter } from 'events';
33
+ import { createHash, randomBytes } from 'crypto';
34
+
35
+ // ============================================
36
+ // TYPE DEFINITIONS (JSDoc)
37
+ // ============================================
38
+
39
+ /**
40
+ * @typedef {Object} AdapterInfo
41
+ * @property {string} id - Unique adapter identifier
42
+ * @property {string} name - Human-readable name
43
+ * @property {string} description - Detailed description
44
+ * @property {string} author - Author name or ID
45
+ * @property {string} authorId - Unique author identifier
46
+ * @property {string} baseModel - Base model identifier
47
+ * @property {string} domain - Primary domain category
48
+ * @property {string[]} tags - Searchable tags
49
+ * @property {number} rating - Average rating (0-5)
50
+ * @property {number} ratingCount - Number of ratings
51
+ * @property {number} downloads - Total download count
52
+ * @property {number} size - Size in bytes
53
+ * @property {string} version - Adapter version
54
+ * @property {string} license - License identifier
55
+ * @property {number} createdAt - Creation timestamp
56
+ * @property {number} updatedAt - Last update timestamp
57
+ * @property {Object} config - LoRA configuration
58
+ * @property {Object} stats - Training statistics
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} BrowseOptions
63
+ * @property {string} [domain] - Filter by domain
64
+ * @property {string} [baseModel] - Filter by base model
65
+ * @property {string} [query] - Search query
66
+ * @property {string[]} [tags] - Filter by tags
67
+ * @property {string} [sort='downloads'] - Sort order
68
+ * @property {number} [limit=20] - Results per page
69
+ * @property {number} [offset=0] - Pagination offset
70
+ * @property {number} [minRating=0] - Minimum rating filter
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} UploadOptions
75
+ * @property {string} name - Adapter name
76
+ * @property {string} description - Adapter description
77
+ * @property {string} domain - Primary domain category
78
+ * @property {string[]} [tags=[]] - Searchable tags
79
+ * @property {string} [license='MIT'] - License identifier
80
+ * @property {boolean} [public=true] - Public visibility
81
+ */
82
+
83
+ /**
84
+ * @typedef {Object} Review
85
+ * @property {string} id - Review ID
86
+ * @property {string} adapterId - Adapter ID
87
+ * @property {string} authorId - Review author ID
88
+ * @property {string} authorName - Review author name
89
+ * @property {number} rating - Rating (1-5)
90
+ * @property {string} comment - Review comment
91
+ * @property {number} createdAt - Creation timestamp
92
+ * @property {number} [helpful=0] - Helpful votes
93
+ */
94
+
95
+ // ============================================
96
+ // CONSTANTS
97
+ // ============================================
98
+
99
+ /**
100
+ * Available domain categories for adapters
101
+ */
102
+ export const ADAPTER_DOMAINS = {
103
+ code: {
104
+ name: 'Code & Programming',
105
+ description: 'Code generation, completion, and programming assistance',
106
+ icon: 'code',
107
+ subdomains: ['python', 'javascript', 'rust', 'go', 'sql', 'general'],
108
+ },
109
+ writing: {
110
+ name: 'Creative Writing',
111
+ description: 'Story writing, poetry, and creative content',
112
+ icon: 'pen',
113
+ subdomains: ['fiction', 'poetry', 'technical', 'copywriting', 'academic'],
114
+ },
115
+ math: {
116
+ name: 'Mathematics',
117
+ description: 'Mathematical reasoning and problem solving',
118
+ icon: 'calculator',
119
+ subdomains: ['algebra', 'calculus', 'statistics', 'geometry', 'logic'],
120
+ },
121
+ science: {
122
+ name: 'Science',
123
+ description: 'Scientific knowledge and reasoning',
124
+ icon: 'flask',
125
+ subdomains: ['physics', 'chemistry', 'biology', 'medicine', 'engineering'],
126
+ },
127
+ language: {
128
+ name: 'Language',
129
+ description: 'Language learning and translation',
130
+ icon: 'globe',
131
+ subdomains: ['translation', 'grammar', 'vocabulary', 'conversation'],
132
+ },
133
+ business: {
134
+ name: 'Business',
135
+ description: 'Business writing and analysis',
136
+ icon: 'briefcase',
137
+ subdomains: ['email', 'reports', 'analysis', 'marketing', 'legal'],
138
+ },
139
+ assistant: {
140
+ name: 'General Assistant',
141
+ description: 'General-purpose assistants and chatbots',
142
+ icon: 'robot',
143
+ subdomains: ['helpful', 'concise', 'detailed', 'friendly', 'formal'],
144
+ },
145
+ roleplay: {
146
+ name: 'Roleplay',
147
+ description: 'Character and roleplay adaptations',
148
+ icon: 'theater',
149
+ subdomains: ['characters', 'games', 'educational', 'simulation'],
150
+ },
151
+ };
152
+
153
+ /**
154
+ * Default hub configuration
155
+ */
156
+ const DEFAULT_HUB_CONFIG = {
157
+ apiEndpoint: 'https://hub.ruvector.dev/api',
158
+ storageEndpoint: 'https://storage.ruvector.dev',
159
+ cacheDir: '.ruvector/adapter-cache',
160
+ maxCacheSize: 500 * 1024 * 1024, // 500MB
161
+ enableOffline: true,
162
+ autoUpdate: true,
163
+ };
164
+
165
+ // ============================================
166
+ // ADAPTERHUB CLASS
167
+ // ============================================
168
+
169
+ /**
170
+ * AdapterHub - Central registry for community adapters
171
+ *
172
+ * Provides a complete ecosystem for discovering, sharing, and managing
173
+ * LoRA adapters. Supports offline caching, ratings/reviews, and version
174
+ * management.
175
+ *
176
+ * @extends EventEmitter
177
+ */
178
+ export class AdapterHub extends EventEmitter {
179
+ /**
180
+ * Create an AdapterHub instance
181
+ *
182
+ * @param {Object} [config={}] - Hub configuration
183
+ */
184
+ constructor(config = {}) {
185
+ super();
186
+
187
+ this.config = { ...DEFAULT_HUB_CONFIG, ...config };
188
+ this.userId = config.userId || `anon-${randomBytes(8).toString('hex')}`;
189
+
190
+ // Local cache of adapter metadata
191
+ this.cache = new Map();
192
+
193
+ // Downloaded adapters
194
+ this.downloaded = new Map();
195
+
196
+ // User's own adapters
197
+ this.myAdapters = new Map();
198
+
199
+ // Reviews cache
200
+ this.reviews = new Map();
201
+
202
+ // Stats
203
+ this.stats = {
204
+ totalBrowses: 0,
205
+ totalDownloads: 0,
206
+ totalUploads: 0,
207
+ cacheHits: 0,
208
+ cacheMisses: 0,
209
+ };
210
+
211
+ // Initialize local storage for offline mode
212
+ this._initLocalStorage();
213
+ }
214
+
215
+ /**
216
+ * Initialize local storage for offline caching
217
+ * @private
218
+ */
219
+ async _initLocalStorage() {
220
+ try {
221
+ if (typeof localStorage !== 'undefined') {
222
+ // Browser environment
223
+ const cached = localStorage.getItem('ruvector-adapter-hub-cache');
224
+ if (cached) {
225
+ const data = JSON.parse(cached);
226
+ for (const [id, info] of Object.entries(data.adapters || {})) {
227
+ this.cache.set(id, info);
228
+ }
229
+ }
230
+ } else if (typeof process !== 'undefined') {
231
+ // Node.js environment
232
+ const fs = await import('fs/promises');
233
+ const path = await import('path');
234
+ const cacheFile = path.join(this.config.cacheDir, 'hub-cache.json');
235
+
236
+ try {
237
+ const content = await fs.readFile(cacheFile, 'utf-8');
238
+ const data = JSON.parse(content);
239
+ for (const [id, info] of Object.entries(data.adapters || {})) {
240
+ this.cache.set(id, info);
241
+ }
242
+ } catch {
243
+ // Cache file doesn't exist yet
244
+ }
245
+ }
246
+ } catch (error) {
247
+ console.error('[AdapterHub] Failed to initialize local storage:', error.message);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Save cache to local storage
253
+ * @private
254
+ */
255
+ async _saveLocalStorage() {
256
+ try {
257
+ const data = {
258
+ adapters: Object.fromEntries(this.cache),
259
+ timestamp: Date.now(),
260
+ };
261
+
262
+ if (typeof localStorage !== 'undefined') {
263
+ localStorage.setItem('ruvector-adapter-hub-cache', JSON.stringify(data));
264
+ } else if (typeof process !== 'undefined') {
265
+ const fs = await import('fs/promises');
266
+ const path = await import('path');
267
+ await fs.mkdir(this.config.cacheDir, { recursive: true });
268
+ const cacheFile = path.join(this.config.cacheDir, 'hub-cache.json');
269
+ await fs.writeFile(cacheFile, JSON.stringify(data, null, 2));
270
+ }
271
+ } catch (error) {
272
+ console.error('[AdapterHub] Failed to save local storage:', error.message);
273
+ }
274
+ }
275
+
276
+ // ============================================
277
+ // BROWSING AND DISCOVERY
278
+ // ============================================
279
+
280
+ /**
281
+ * Browse available adapters with filtering and sorting
282
+ *
283
+ * @param {BrowseOptions} [options={}] - Browse options
284
+ * @returns {Promise<{adapters: AdapterInfo[], total: number, hasMore: boolean}>}
285
+ *
286
+ * @example
287
+ * ```javascript
288
+ * // Browse code adapters sorted by rating
289
+ * const results = await hub.browse({
290
+ * domain: 'code',
291
+ * sort: 'rating',
292
+ * limit: 10
293
+ * });
294
+ *
295
+ * for (const adapter of results.adapters) {
296
+ * console.log(`${adapter.name}: ${adapter.rating} stars`);
297
+ * }
298
+ * ```
299
+ */
300
+ async browse(options = {}) {
301
+ const opts = {
302
+ domain: null,
303
+ baseModel: null,
304
+ query: null,
305
+ tags: [],
306
+ sort: 'downloads',
307
+ limit: 20,
308
+ offset: 0,
309
+ minRating: 0,
310
+ ...options,
311
+ };
312
+
313
+ this.stats.totalBrowses++;
314
+ this.emit('browse:start', opts);
315
+
316
+ // Filter adapters from cache
317
+ let adapters = Array.from(this.cache.values());
318
+
319
+ // Apply filters
320
+ if (opts.domain) {
321
+ adapters = adapters.filter(a => a.domain === opts.domain);
322
+ }
323
+ if (opts.baseModel) {
324
+ adapters = adapters.filter(a => a.baseModel === opts.baseModel);
325
+ }
326
+ if (opts.minRating > 0) {
327
+ adapters = adapters.filter(a => a.rating >= opts.minRating);
328
+ }
329
+ if (opts.tags && opts.tags.length > 0) {
330
+ adapters = adapters.filter(a =>
331
+ opts.tags.some(tag => a.tags?.includes(tag))
332
+ );
333
+ }
334
+ if (opts.query) {
335
+ const query = opts.query.toLowerCase();
336
+ adapters = adapters.filter(a =>
337
+ a.name?.toLowerCase().includes(query) ||
338
+ a.description?.toLowerCase().includes(query) ||
339
+ a.tags?.some(t => t.toLowerCase().includes(query))
340
+ );
341
+ }
342
+
343
+ // Sort
344
+ switch (opts.sort) {
345
+ case 'rating':
346
+ adapters.sort((a, b) => (b.rating || 0) - (a.rating || 0));
347
+ break;
348
+ case 'downloads':
349
+ adapters.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
350
+ break;
351
+ case 'recent':
352
+ adapters.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
353
+ break;
354
+ case 'updated':
355
+ adapters.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
356
+ break;
357
+ case 'name':
358
+ adapters.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
359
+ break;
360
+ }
361
+
362
+ // Paginate
363
+ const total = adapters.length;
364
+ const paged = adapters.slice(opts.offset, opts.offset + opts.limit);
365
+
366
+ const result = {
367
+ adapters: paged,
368
+ total,
369
+ hasMore: opts.offset + opts.limit < total,
370
+ offset: opts.offset,
371
+ limit: opts.limit,
372
+ };
373
+
374
+ this.emit('browse:complete', result);
375
+ return result;
376
+ }
377
+
378
+ /**
379
+ * Search adapters by query string
380
+ *
381
+ * @param {string} query - Search query
382
+ * @param {Object} [options={}] - Additional filters
383
+ * @returns {Promise<AdapterInfo[]>}
384
+ *
385
+ * @example
386
+ * ```javascript
387
+ * const results = await hub.search('python code completion');
388
+ * ```
389
+ */
390
+ async search(query, options = {}) {
391
+ return this.browse({ ...options, query });
392
+ }
393
+
394
+ /**
395
+ * Get featured/recommended adapters
396
+ *
397
+ * @param {number} [limit=10] - Number of adapters to return
398
+ * @returns {Promise<AdapterInfo[]>}
399
+ */
400
+ async getFeatured(limit = 10) {
401
+ const result = await this.browse({
402
+ sort: 'rating',
403
+ minRating: 4.0,
404
+ limit,
405
+ });
406
+ return result.adapters;
407
+ }
408
+
409
+ /**
410
+ * Get trending adapters (most downloaded recently)
411
+ *
412
+ * @param {number} [limit=10] - Number of adapters to return
413
+ * @returns {Promise<AdapterInfo[]>}
414
+ */
415
+ async getTrending(limit = 10) {
416
+ const result = await this.browse({
417
+ sort: 'downloads',
418
+ limit,
419
+ });
420
+ return result.adapters;
421
+ }
422
+
423
+ /**
424
+ * Get adapters by domain category
425
+ *
426
+ * @param {string} domain - Domain category
427
+ * @param {Object} [options={}] - Additional options
428
+ * @returns {Promise<AdapterInfo[]>}
429
+ */
430
+ async getByDomain(domain, options = {}) {
431
+ const result = await this.browse({ ...options, domain });
432
+ return result.adapters;
433
+ }
434
+
435
+ /**
436
+ * Get available domain categories with counts
437
+ *
438
+ * @returns {Promise<Array<{domain: string, count: number, info: Object}>>}
439
+ */
440
+ async getDomains() {
441
+ const counts = {};
442
+ for (const adapter of this.cache.values()) {
443
+ counts[adapter.domain] = (counts[adapter.domain] || 0) + 1;
444
+ }
445
+
446
+ return Object.entries(ADAPTER_DOMAINS).map(([id, info]) => ({
447
+ domain: id,
448
+ count: counts[id] || 0,
449
+ ...info,
450
+ }));
451
+ }
452
+
453
+ // ============================================
454
+ // DOWNLOAD AND APPLY
455
+ // ============================================
456
+
457
+ /**
458
+ * Download an adapter by ID
459
+ *
460
+ * @param {string} adapterId - Adapter identifier
461
+ * @param {Object} [options={}] - Download options
462
+ * @returns {Promise<Object>} Adapter data
463
+ *
464
+ * @example
465
+ * ```javascript
466
+ * const adapter = await hub.download('code-assistant-v2');
467
+ * await myLoRA.loadAdapter(adapter);
468
+ * ```
469
+ */
470
+ async download(adapterId, options = {}) {
471
+ this.stats.totalDownloads++;
472
+ this.emit('download:start', { adapterId });
473
+
474
+ // Check local cache first
475
+ if (this.downloaded.has(adapterId)) {
476
+ this.stats.cacheHits++;
477
+ const cached = this.downloaded.get(adapterId);
478
+ this.emit('download:complete', { adapterId, cached: true });
479
+ return cached;
480
+ }
481
+
482
+ this.stats.cacheMisses++;
483
+
484
+ // Get adapter info
485
+ const info = this.cache.get(adapterId);
486
+ if (!info) {
487
+ throw new Error(`Adapter not found: ${adapterId}`);
488
+ }
489
+
490
+ // Simulate download (in production, would fetch from storage)
491
+ const adapterData = this._generateMockAdapter(info);
492
+
493
+ // Cache downloaded adapter
494
+ this.downloaded.set(adapterId, adapterData);
495
+
496
+ // Update download count
497
+ info.downloads = (info.downloads || 0) + 1;
498
+ this.cache.set(adapterId, info);
499
+ await this._saveLocalStorage();
500
+
501
+ this.emit('download:complete', { adapterId, cached: false, size: JSON.stringify(adapterData).length });
502
+ return adapterData;
503
+ }
504
+
505
+ /**
506
+ * Generate mock adapter data for demo purposes
507
+ * @private
508
+ */
509
+ _generateMockAdapter(info) {
510
+ const rank = info.config?.rank || 4;
511
+ const dim = info.config?.embeddingDim || 384;
512
+
513
+ const adapters = {};
514
+ for (const module of ['query', 'value', 'key', 'dense']) {
515
+ adapters[module] = {
516
+ loraA: this._generateRandomMatrix(dim, rank),
517
+ loraB: this._generateRandomMatrix(rank, dim),
518
+ scaling: (info.config?.alpha || 8) / rank,
519
+ };
520
+ }
521
+
522
+ return {
523
+ version: '1.0.0',
524
+ format: 'microlora',
525
+ metadata: {
526
+ id: info.id,
527
+ name: info.name,
528
+ description: info.description,
529
+ baseModel: info.baseModel,
530
+ domain: info.domain,
531
+ rank: rank,
532
+ alpha: info.config?.alpha || 8,
533
+ trainingSamples: info.stats?.trainingSamples || 0,
534
+ trainingEpochs: info.stats?.trainingEpochs || 0,
535
+ createdAt: info.createdAt,
536
+ version: info.version,
537
+ },
538
+ config: info.config,
539
+ baseModel: info.baseModel,
540
+ adapters,
541
+ stats: info.stats,
542
+ createdAt: info.createdAt,
543
+ savedAt: Date.now(),
544
+ };
545
+ }
546
+
547
+ /**
548
+ * Generate random matrix for mock data
549
+ * @private
550
+ */
551
+ _generateRandomMatrix(rows, cols) {
552
+ const matrix = [];
553
+ const std = Math.sqrt(2 / (rows + cols)) * 0.1;
554
+ for (let i = 0; i < rows; i++) {
555
+ const row = [];
556
+ for (let j = 0; j < cols; j++) {
557
+ row.push((Math.random() - 0.5) * 2 * std);
558
+ }
559
+ matrix.push(row);
560
+ }
561
+ return matrix;
562
+ }
563
+
564
+ /**
565
+ * Check if an adapter is downloaded locally
566
+ *
567
+ * @param {string} adapterId - Adapter identifier
568
+ * @returns {boolean}
569
+ */
570
+ isDownloaded(adapterId) {
571
+ return this.downloaded.has(adapterId);
572
+ }
573
+
574
+ /**
575
+ * Remove a downloaded adapter from local cache
576
+ *
577
+ * @param {string} adapterId - Adapter identifier
578
+ */
579
+ removeDownloaded(adapterId) {
580
+ this.downloaded.delete(adapterId);
581
+ this.emit('adapter:removed', { adapterId });
582
+ }
583
+
584
+ // ============================================
585
+ // UPLOAD AND SHARE
586
+ // ============================================
587
+
588
+ /**
589
+ * Upload an adapter to the hub
590
+ *
591
+ * @param {Object} adapter - MicroLoRA instance or adapter data
592
+ * @param {UploadOptions} options - Upload options
593
+ * @returns {Promise<AdapterInfo>} Uploaded adapter info
594
+ *
595
+ * @example
596
+ * ```javascript
597
+ * const info = await hub.upload(myLoRA, {
598
+ * name: 'Python Code Assistant',
599
+ * description: 'Specialized for Python coding tasks',
600
+ * domain: 'code',
601
+ * tags: ['python', 'coding', 'assistant']
602
+ * });
603
+ * console.log(`Uploaded: ${info.id}`);
604
+ * ```
605
+ */
606
+ async upload(adapter, options) {
607
+ const opts = {
608
+ name: 'Untitled Adapter',
609
+ description: '',
610
+ domain: 'general',
611
+ tags: [],
612
+ license: 'MIT',
613
+ public: true,
614
+ ...options,
615
+ };
616
+
617
+ this.stats.totalUploads++;
618
+ this.emit('upload:start', { name: opts.name });
619
+
620
+ // Get adapter data
621
+ let adapterData;
622
+ if (typeof adapter.saveAdapter === 'function') {
623
+ adapterData = await adapter.saveAdapter();
624
+ } else {
625
+ adapterData = adapter;
626
+ }
627
+
628
+ // Generate unique ID
629
+ const id = `${opts.domain}-${opts.name.toLowerCase().replace(/\s+/g, '-')}-${randomBytes(4).toString('hex')}`;
630
+
631
+ // Create adapter info
632
+ const info = {
633
+ id,
634
+ name: opts.name,
635
+ description: opts.description,
636
+ author: this.userId,
637
+ authorId: this.userId,
638
+ baseModel: adapterData.baseModel || 'unknown',
639
+ domain: opts.domain,
640
+ tags: opts.tags,
641
+ rating: 0,
642
+ ratingCount: 0,
643
+ downloads: 0,
644
+ size: JSON.stringify(adapterData).length,
645
+ version: adapterData.version || '1.0.0',
646
+ license: opts.license,
647
+ createdAt: Date.now(),
648
+ updatedAt: Date.now(),
649
+ config: adapterData.config,
650
+ stats: adapterData.stats,
651
+ public: opts.public,
652
+ };
653
+
654
+ // Store in cache and my adapters
655
+ this.cache.set(id, info);
656
+ this.myAdapters.set(id, { info, data: adapterData });
657
+ await this._saveLocalStorage();
658
+
659
+ this.emit('upload:complete', info);
660
+ return info;
661
+ }
662
+
663
+ /**
664
+ * Update an uploaded adapter
665
+ *
666
+ * @param {string} adapterId - Adapter to update
667
+ * @param {Object} adapter - New adapter data
668
+ * @param {Object} [options={}] - Update options
669
+ * @returns {Promise<AdapterInfo>}
670
+ */
671
+ async update(adapterId, adapter, options = {}) {
672
+ const existing = this.myAdapters.get(adapterId);
673
+ if (!existing) {
674
+ throw new Error(`Adapter not found in your uploads: ${adapterId}`);
675
+ }
676
+
677
+ let adapterData;
678
+ if (typeof adapter.saveAdapter === 'function') {
679
+ adapterData = await adapter.saveAdapter();
680
+ } else {
681
+ adapterData = adapter;
682
+ }
683
+
684
+ // Update info
685
+ const info = {
686
+ ...existing.info,
687
+ ...options,
688
+ updatedAt: Date.now(),
689
+ size: JSON.stringify(adapterData).length,
690
+ config: adapterData.config,
691
+ stats: adapterData.stats,
692
+ };
693
+
694
+ // Increment version
695
+ const versionParts = (info.version || '1.0.0').split('.').map(Number);
696
+ versionParts[2]++;
697
+ info.version = versionParts.join('.');
698
+
699
+ // Update cache
700
+ this.cache.set(adapterId, info);
701
+ this.myAdapters.set(adapterId, { info, data: adapterData });
702
+ await this._saveLocalStorage();
703
+
704
+ this.emit('update:complete', info);
705
+ return info;
706
+ }
707
+
708
+ /**
709
+ * Delete an uploaded adapter
710
+ *
711
+ * @param {string} adapterId - Adapter to delete
712
+ * @returns {Promise<boolean>}
713
+ */
714
+ async delete(adapterId) {
715
+ if (!this.myAdapters.has(adapterId)) {
716
+ throw new Error(`Adapter not found in your uploads: ${adapterId}`);
717
+ }
718
+
719
+ this.cache.delete(adapterId);
720
+ this.myAdapters.delete(adapterId);
721
+ this.downloaded.delete(adapterId);
722
+ this.reviews.delete(adapterId);
723
+
724
+ await this._saveLocalStorage();
725
+ this.emit('delete:complete', { adapterId });
726
+ return true;
727
+ }
728
+
729
+ /**
730
+ * Get user's uploaded adapters
731
+ *
732
+ * @returns {AdapterInfo[]}
733
+ */
734
+ getMyAdapters() {
735
+ return Array.from(this.myAdapters.values()).map(a => a.info);
736
+ }
737
+
738
+ // ============================================
739
+ // RATINGS AND REVIEWS
740
+ // ============================================
741
+
742
+ /**
743
+ * Rate an adapter
744
+ *
745
+ * @param {string} adapterId - Adapter to rate
746
+ * @param {number} rating - Rating (1-5)
747
+ * @returns {Promise<void>}
748
+ *
749
+ * @example
750
+ * ```javascript
751
+ * await hub.rate('code-assistant-v2', 5);
752
+ * ```
753
+ */
754
+ async rate(adapterId, rating) {
755
+ if (rating < 1 || rating > 5) {
756
+ throw new Error('Rating must be between 1 and 5');
757
+ }
758
+
759
+ const info = this.cache.get(adapterId);
760
+ if (!info) {
761
+ throw new Error(`Adapter not found: ${adapterId}`);
762
+ }
763
+
764
+ // Update rating (running average)
765
+ const totalRating = (info.rating || 0) * (info.ratingCount || 0);
766
+ info.ratingCount = (info.ratingCount || 0) + 1;
767
+ info.rating = (totalRating + rating) / info.ratingCount;
768
+
769
+ this.cache.set(adapterId, info);
770
+ await this._saveLocalStorage();
771
+
772
+ this.emit('rating:added', { adapterId, rating, newRating: info.rating });
773
+ }
774
+
775
+ /**
776
+ * Add a review for an adapter
777
+ *
778
+ * @param {string} adapterId - Adapter to review
779
+ * @param {number} rating - Rating (1-5)
780
+ * @param {string} comment - Review comment
781
+ * @returns {Promise<Review>}
782
+ *
783
+ * @example
784
+ * ```javascript
785
+ * const review = await hub.review('code-assistant-v2', 5, 'Great for Python!');
786
+ * ```
787
+ */
788
+ async review(adapterId, rating, comment) {
789
+ // Add rating first
790
+ await this.rate(adapterId, rating);
791
+
792
+ // Create review
793
+ const reviewData = {
794
+ id: `review-${randomBytes(6).toString('hex')}`,
795
+ adapterId,
796
+ authorId: this.userId,
797
+ authorName: `User-${this.userId.slice(0, 8)}`,
798
+ rating,
799
+ comment,
800
+ createdAt: Date.now(),
801
+ helpful: 0,
802
+ };
803
+
804
+ // Store review
805
+ if (!this.reviews.has(adapterId)) {
806
+ this.reviews.set(adapterId, []);
807
+ }
808
+ this.reviews.get(adapterId).push(reviewData);
809
+
810
+ this.emit('review:added', reviewData);
811
+ return reviewData;
812
+ }
813
+
814
+ /**
815
+ * Get reviews for an adapter
816
+ *
817
+ * @param {string} adapterId - Adapter ID
818
+ * @param {Object} [options={}] - Options
819
+ * @returns {Promise<Review[]>}
820
+ */
821
+ async getReviews(adapterId, options = {}) {
822
+ const { sort = 'recent', limit = 20 } = options;
823
+
824
+ const adapterReviews = this.reviews.get(adapterId) || [];
825
+
826
+ // Sort
827
+ const sorted = [...adapterReviews];
828
+ switch (sort) {
829
+ case 'helpful':
830
+ sorted.sort((a, b) => (b.helpful || 0) - (a.helpful || 0));
831
+ break;
832
+ case 'rating':
833
+ sorted.sort((a, b) => b.rating - a.rating);
834
+ break;
835
+ case 'recent':
836
+ default:
837
+ sorted.sort((a, b) => b.createdAt - a.createdAt);
838
+ }
839
+
840
+ return sorted.slice(0, limit);
841
+ }
842
+
843
+ /**
844
+ * Mark a review as helpful
845
+ *
846
+ * @param {string} reviewId - Review ID
847
+ */
848
+ async markHelpful(reviewId) {
849
+ for (const reviews of this.reviews.values()) {
850
+ const review = reviews.find(r => r.id === reviewId);
851
+ if (review) {
852
+ review.helpful = (review.helpful || 0) + 1;
853
+ this.emit('review:helpful', { reviewId, helpful: review.helpful });
854
+ return;
855
+ }
856
+ }
857
+ }
858
+
859
+ // ============================================
860
+ // COLLECTIONS AND FAVORITES
861
+ // ============================================
862
+
863
+ /**
864
+ * Create a collection of adapters
865
+ *
866
+ * @param {string} name - Collection name
867
+ * @param {string} [description=''] - Collection description
868
+ * @returns {Object} Collection info
869
+ */
870
+ createCollection(name, description = '') {
871
+ const id = `collection-${randomBytes(6).toString('hex')}`;
872
+ const collection = {
873
+ id,
874
+ name,
875
+ description,
876
+ adapters: [],
877
+ createdAt: Date.now(),
878
+ updatedAt: Date.now(),
879
+ };
880
+
881
+ this.emit('collection:created', collection);
882
+ return collection;
883
+ }
884
+
885
+ /**
886
+ * Add adapter to favorites
887
+ *
888
+ * @param {string} adapterId - Adapter to favorite
889
+ */
890
+ addFavorite(adapterId) {
891
+ this.emit('favorite:added', { adapterId });
892
+ }
893
+
894
+ /**
895
+ * Remove adapter from favorites
896
+ *
897
+ * @param {string} adapterId - Adapter to unfavorite
898
+ */
899
+ removeFavorite(adapterId) {
900
+ this.emit('favorite:removed', { adapterId });
901
+ }
902
+
903
+ // ============================================
904
+ // UTILITY METHODS
905
+ // ============================================
906
+
907
+ /**
908
+ * Get detailed info about an adapter
909
+ *
910
+ * @param {string} adapterId - Adapter ID
911
+ * @returns {Promise<AdapterInfo|null>}
912
+ */
913
+ async getAdapterInfo(adapterId) {
914
+ return this.cache.get(adapterId) || null;
915
+ }
916
+
917
+ /**
918
+ * Check if an adapter exists
919
+ *
920
+ * @param {string} adapterId - Adapter ID
921
+ * @returns {boolean}
922
+ */
923
+ exists(adapterId) {
924
+ return this.cache.has(adapterId);
925
+ }
926
+
927
+ /**
928
+ * Get hub statistics
929
+ *
930
+ * @returns {Object}
931
+ */
932
+ getStats() {
933
+ return {
934
+ ...this.stats,
935
+ totalAdapters: this.cache.size,
936
+ downloadedAdapters: this.downloaded.size,
937
+ myAdapters: this.myAdapters.size,
938
+ totalReviews: Array.from(this.reviews.values()).reduce((sum, r) => sum + r.length, 0),
939
+ };
940
+ }
941
+
942
+ /**
943
+ * Clear all caches
944
+ */
945
+ async clearCache() {
946
+ this.downloaded.clear();
947
+ this.emit('cache:cleared');
948
+ }
949
+
950
+ /**
951
+ * Seed hub with sample adapters for demo purposes
952
+ *
953
+ * @param {number} [count=20] - Number of sample adapters
954
+ */
955
+ async seedSampleAdapters(count = 20) {
956
+ const domains = Object.keys(ADAPTER_DOMAINS);
957
+ const models = ['phi-1.5-int4', 'distilgpt2', 'gpt2', 'starcoder-tiny'];
958
+ const adjectives = ['Advanced', 'Pro', 'Ultra', 'Smart', 'Fast', 'Accurate', 'Helpful'];
959
+ const nouns = ['Assistant', 'Helper', 'Expert', 'Companion', 'Generator', 'Wizard'];
960
+
961
+ for (let i = 0; i < count; i++) {
962
+ const domain = domains[Math.floor(Math.random() * domains.length)];
963
+ const model = models[Math.floor(Math.random() * models.length)];
964
+ const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
965
+ const noun = nouns[Math.floor(Math.random() * nouns.length)];
966
+ const name = `${adj} ${ADAPTER_DOMAINS[domain].name.split(' ')[0]} ${noun}`;
967
+
968
+ const info = {
969
+ id: `sample-${domain}-${randomBytes(4).toString('hex')}`,
970
+ name,
971
+ description: `A ${adj.toLowerCase()} adapter for ${ADAPTER_DOMAINS[domain].description.toLowerCase()}`,
972
+ author: `sample-author-${i % 5}`,
973
+ authorId: `sample-author-${i % 5}`,
974
+ baseModel: model,
975
+ domain,
976
+ tags: [domain, ...ADAPTER_DOMAINS[domain].subdomains.slice(0, 2)],
977
+ rating: 3 + Math.random() * 2,
978
+ ratingCount: Math.floor(Math.random() * 100) + 1,
979
+ downloads: Math.floor(Math.random() * 10000),
980
+ size: Math.floor(Math.random() * 50000) + 10000,
981
+ version: `1.${Math.floor(Math.random() * 10)}.0`,
982
+ license: 'MIT',
983
+ createdAt: Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000),
984
+ updatedAt: Date.now() - Math.floor(Math.random() * 7 * 24 * 60 * 60 * 1000),
985
+ config: {
986
+ rank: [4, 8, 16][Math.floor(Math.random() * 3)],
987
+ alpha: [8, 16, 32][Math.floor(Math.random() * 3)],
988
+ embeddingDim: 384,
989
+ },
990
+ stats: {
991
+ trainingSamples: Math.floor(Math.random() * 10000) + 100,
992
+ trainingEpochs: Math.floor(Math.random() * 50) + 5,
993
+ },
994
+ };
995
+
996
+ this.cache.set(info.id, info);
997
+ }
998
+
999
+ await this._saveLocalStorage();
1000
+ this.emit('seed:complete', { count });
1001
+ }
1002
+ }
1003
+
1004
+ // ============================================
1005
+ // EXPORTS
1006
+ // ============================================
1007
+
1008
+ export default AdapterHub;