@retrivora-ai/rag-engine 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{ChromaDBProvider-QNI7UCX4.mjs → ChromaDBProvider-2JZSBOQX.mjs} +1 -1
  2. package/dist/{MilvusProvider-OO6QGZDZ.mjs → MilvusProvider-CJDBCBVI.mjs} +1 -1
  3. package/dist/{PineconeProvider-ZRAFNFEC.mjs → PineconeProvider-E6L5Z2FO.mjs} +1 -1
  4. package/dist/{QdrantProvider-NYGHAA7C.mjs → QdrantProvider-JITRNJQN.mjs} +1 -1
  5. package/dist/{RagConfig-hBGXJmSx.d.mts → RagConfig-D_rSf8ep.d.mts} +1 -1
  6. package/dist/{RagConfig-hBGXJmSx.d.ts → RagConfig-D_rSf8ep.d.ts} +1 -1
  7. package/dist/{RedisProvider-ASONNYBI.mjs → RedisProvider-3VKFQXXD.mjs} +1 -1
  8. package/dist/UniversalVectorProvider-E6L4U4OX.mjs +9 -0
  9. package/dist/{WeaviateProvider-PSDCUGC7.mjs → WeaviateProvider-MXIPP44J.mjs} +1 -1
  10. package/dist/{chunk-5K23H7JL.mjs → chunk-26EMHLIN.mjs} +40 -2
  11. package/dist/{chunk-VPNRDXIA.mjs → chunk-7BQI4A5J.mjs} +17 -11
  12. package/dist/{chunk-ZM6TYIDH.mjs → chunk-MFWJZVF3.mjs} +3 -1
  13. package/dist/{chunk-V75V7BT2.mjs → chunk-TSX6DQXX.mjs} +2 -2
  14. package/dist/{chunk-HUGLYKD6.mjs → chunk-XZPVJS2B.mjs} +27 -9
  15. package/dist/chunk-Y6HQZDCJ.mjs +156 -0
  16. package/dist/{chunk-HSBXE2WV.mjs → chunk-YIYDJQJM.mjs} +745 -192
  17. package/dist/{chunk-7NXI6ZWX.mjs → chunk-YST6KYBJ.mjs} +8 -5
  18. package/dist/handlers/index.d.mts +2 -2
  19. package/dist/handlers/index.d.ts +2 -2
  20. package/dist/handlers/index.js +924 -626
  21. package/dist/handlers/index.mjs +1 -2
  22. package/dist/index-BJ8CUArE.d.mts +114 -0
  23. package/dist/index-DtNprGGj.d.ts +114 -0
  24. package/dist/index.d.mts +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/server.d.mts +575 -17
  27. package/dist/server.d.ts +575 -17
  28. package/dist/server.js +1280 -643
  29. package/dist/server.mjs +314 -16
  30. package/package.json +11 -2
  31. package/src/config/ConfigBuilder.ts +373 -0
  32. package/src/config/EmbeddingStrategy.ts +147 -0
  33. package/src/config/serverConfig.ts +51 -17
  34. package/src/core/ConfigValidator.ts +77 -52
  35. package/src/core/Pipeline.ts +28 -26
  36. package/src/core/PluginManager.ts +277 -0
  37. package/src/core/ProviderHealthCheck.ts +79 -140
  38. package/src/core/ProviderRegistry.ts +38 -15
  39. package/src/providers/vectordb/ChromaDBProvider.ts +37 -12
  40. package/src/providers/vectordb/MilvusProvider.ts +25 -10
  41. package/src/providers/vectordb/PineconeProvider.ts +17 -2
  42. package/src/providers/vectordb/QdrantProvider.ts +46 -2
  43. package/src/providers/vectordb/RedisProvider.ts +34 -11
  44. package/src/providers/vectordb/UniversalVectorProvider.ts +220 -0
  45. package/src/providers/vectordb/WeaviateProvider.ts +17 -10
  46. package/src/server.ts +28 -10
  47. package/dist/LLMFactory-JFOY2V4X.mjs +0 -8
  48. package/dist/chunk-JI6VD5TJ.mjs +0 -387
  49. package/dist/index-Bx182KKn.d.ts +0 -64
  50. package/dist/index-Ck2pt7-8.d.mts +0 -64
  51. package/src/test-refactor.ts +0 -59
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Validates RagConfig against provider requirements and best practices.
5
5
  * Returns detailed errors for misconfiguration.
6
+ *
7
+ * Key invariant: option keys validated here MUST match the keys each
8
+ * Provider constructor reads. See each provider's constructor for the
9
+ * canonical option names.
6
10
  */
7
11
 
8
12
  import { RagConfig, VectorDBConfig, LLMConfig, EmbeddingConfig } from '../config/RagConfig';
@@ -22,7 +26,6 @@ export class ConfigValidator {
22
26
  static validate(config: RagConfig): ValidationError[] {
23
27
  const errors: ValidationError[] = [];
24
28
 
25
- // Validate root level
26
29
  if (!config.projectId) {
27
30
  errors.push({
28
31
  field: 'projectId',
@@ -31,31 +34,24 @@ export class ConfigValidator {
31
34
  });
32
35
  }
33
36
 
34
- // Validate Vector DB configuration
35
37
  errors.push(...this.validateVectorDbConfig(config.vectorDb));
36
-
37
- // Validate LLM configuration
38
38
  errors.push(...this.validateLLMConfig(config.llm));
39
39
 
40
- // Validate Embedding configuration (if provided)
41
40
  if (config.embedding) {
42
41
  errors.push(...this.validateEmbeddingConfig(config.embedding));
43
42
  } else if (config.llm.provider === 'anthropic') {
44
- // Anthropic should have a separate embedding provider
45
43
  errors.push({
46
44
  field: 'embedding',
47
45
  message: 'Embedding config is required when using Anthropic LLM',
48
- suggestion: 'Provide embedding config with provider (e.g., "openai")',
46
+ suggestion: 'Provide embedding config with provider (e.g., "openai" or "ollama")',
49
47
  severity: 'error',
50
48
  });
51
49
  }
52
50
 
53
- // Validate UI config if provided
54
51
  if (config.ui) {
55
52
  errors.push(...this.validateUIConfig(config.ui as Record<string, unknown>));
56
53
  }
57
54
 
58
- // Validate RAG tuning if provided
59
55
  if (config.rag) {
60
56
  errors.push(...this.validateRAGConfig(config.rag as Record<string, unknown>));
61
57
  }
@@ -83,7 +79,6 @@ export class ConfigValidator {
83
79
  });
84
80
  }
85
81
 
86
- // Provider-specific validation
87
82
  switch (config.provider) {
88
83
  case 'pinecone':
89
84
  errors.push(...this.validatePineconeConfig(config));
@@ -119,6 +114,10 @@ export class ConfigValidator {
119
114
  return errors;
120
115
  }
121
116
 
117
+ /**
118
+ * Pinecone — only needs apiKey (environment was removed in SDK v7+).
119
+ * Options key: { apiKey: string }
120
+ */
122
121
  private static validatePineconeConfig(config: VectorDBConfig): ValidationError[] {
123
122
  const errors: ValidationError[] = [];
124
123
  const opts = config.options as Record<string, unknown>;
@@ -132,18 +131,13 @@ export class ConfigValidator {
132
131
  });
133
132
  }
134
133
 
135
- if (!opts.environment) {
136
- errors.push({
137
- field: 'vectorDb.options.environment',
138
- message: 'Pinecone environment is required',
139
- suggestion: 'Set PINECONE_ENVIRONMENT environment variable (e.g., "gcp-starter")',
140
- severity: 'error',
141
- });
142
- }
143
-
144
134
  return errors;
145
135
  }
146
136
 
137
+ /**
138
+ * PostgreSQL / pgvector — needs a connection string.
139
+ * Options key: { connectionString: string }
140
+ */
147
141
  private static validatePostgresConfig(config: VectorDBConfig): ValidationError[] {
148
142
  const errors: ValidationError[] = [];
149
143
  const opts = config.options as Record<string, unknown>;
@@ -160,6 +154,10 @@ export class ConfigValidator {
160
154
  return errors;
161
155
  }
162
156
 
157
+ /**
158
+ * MongoDB — needs uri, database, and collection.
159
+ * Options keys: { uri: string, database: string, collection: string }
160
+ */
163
161
  private static validateMongoDBConfig(config: VectorDBConfig): ValidationError[] {
164
162
  const errors: ValidationError[] = [];
165
163
  const opts = config.options as Record<string, unknown>;
@@ -194,24 +192,23 @@ export class ConfigValidator {
194
192
  return errors;
195
193
  }
196
194
 
195
+ /**
196
+ * Milvus — accepts baseUrl OR uri (preferred) OR host+port.
197
+ * MilvusProvider reads opts.baseUrl ?? opts.uri.
198
+ * Options keys: { baseUrl?: string, uri?: string, host?: string, port?: number }
199
+ */
197
200
  private static validateMilvusConfig(config: VectorDBConfig): ValidationError[] {
198
201
  const errors: ValidationError[] = [];
199
202
  const opts = config.options as Record<string, unknown>;
200
203
 
201
- if (!opts.host) {
202
- errors.push({
203
- field: 'vectorDb.options.host',
204
- message: 'Milvus host is required',
205
- suggestion: 'Set MILVUS_HOST environment variable',
206
- severity: 'error',
207
- });
208
- }
204
+ const hasBaseUrl = Boolean(opts.baseUrl || opts.uri);
205
+ const hasHostPort = Boolean(opts.host && opts.port);
209
206
 
210
- if (!opts.port) {
207
+ if (!hasBaseUrl && !hasHostPort) {
211
208
  errors.push({
212
- field: 'vectorDb.options.port',
213
- message: 'Milvus port is required',
214
- suggestion: 'Set MILVUS_PORT environment variable (default: 19530)',
209
+ field: 'vectorDb.options.baseUrl',
210
+ message: 'Milvus connection is required: provide baseUrl (e.g., "http://localhost:19530") or host + port',
211
+ suggestion: 'Set MILVUS_URL environment variable',
215
212
  severity: 'error',
216
213
  });
217
214
  }
@@ -219,6 +216,10 @@ export class ConfigValidator {
219
216
  return errors;
220
217
  }
221
218
 
219
+ /**
220
+ * Qdrant — needs baseUrl (or url alias).
221
+ * Options keys: { baseUrl: string, apiKey?: string }
222
+ */
222
223
  private static validateQdrantConfig(config: VectorDBConfig): ValidationError[] {
223
224
  const errors: ValidationError[] = [];
224
225
  const opts = config.options as Record<string, unknown>;
@@ -235,15 +236,20 @@ export class ConfigValidator {
235
236
  return errors;
236
237
  }
237
238
 
239
+ /**
240
+ * ChromaDB — accepts baseUrl OR host.
241
+ * ChromaDBProvider reads opts.baseUrl.
242
+ * Options keys: { baseUrl?: string, host?: string, port?: number }
243
+ */
238
244
  private static validateChromaDBConfig(config: VectorDBConfig): ValidationError[] {
239
245
  const errors: ValidationError[] = [];
240
246
  const opts = config.options as Record<string, unknown>;
241
247
 
242
- if (!opts.host && !opts.path) {
248
+ if (!opts.baseUrl && !opts.host) {
243
249
  errors.push({
244
- field: 'vectorDb.options.host',
245
- message: 'ChromaDB host or path is required',
246
- suggestion: 'Set CHROMADB_HOST or CHROMADB_PATH environment variable',
250
+ field: 'vectorDb.options.baseUrl',
251
+ message: 'ChromaDB connection is required: provide baseUrl (e.g., "http://localhost:8000") or host',
252
+ suggestion: 'Set CHROMADB_URL environment variable',
247
253
  severity: 'error',
248
254
  });
249
255
  }
@@ -251,15 +257,23 @@ export class ConfigValidator {
251
257
  return errors;
252
258
  }
253
259
 
260
+ /**
261
+ * Redis — accepts baseUrl OR url OR host+port.
262
+ * RedisProvider reads opts.baseUrl.
263
+ * Options keys: { baseUrl?: string, url?: string, host?: string, port?: number, apiKey?: string }
264
+ */
254
265
  private static validateRedisConfig(config: VectorDBConfig): ValidationError[] {
255
266
  const errors: ValidationError[] = [];
256
267
  const opts = config.options as Record<string, unknown>;
257
268
 
258
- if (!opts.url && (!opts.host || !opts.port)) {
269
+ const hasUrl = Boolean(opts.baseUrl || opts.url);
270
+ const hasHostPort = Boolean(opts.host && opts.port);
271
+
272
+ if (!hasUrl && !hasHostPort) {
259
273
  errors.push({
260
- field: 'vectorDb.options.url',
261
- message: 'Redis connection URL or host:port is required',
262
- suggestion: 'Set REDIS_URL or REDIS_HOST/REDIS_PORT environment variables',
274
+ field: 'vectorDb.options.baseUrl',
275
+ message: 'Redis connection is required: provide baseUrl/url or host + port',
276
+ suggestion: 'Set REDIS_URL environment variable',
263
277
  severity: 'error',
264
278
  });
265
279
  }
@@ -267,13 +281,18 @@ export class ConfigValidator {
267
281
  return errors;
268
282
  }
269
283
 
284
+ /**
285
+ * Weaviate — accepts baseUrl OR url.
286
+ * WeaviateProvider reads opts.baseUrl.
287
+ * Options keys: { baseUrl?: string, url?: string, apiKey?: string }
288
+ */
270
289
  private static validateWeaviateConfig(config: VectorDBConfig): ValidationError[] {
271
290
  const errors: ValidationError[] = [];
272
291
  const opts = config.options as Record<string, unknown>;
273
292
 
274
- if (!opts.url) {
293
+ if (!opts.baseUrl && !opts.url) {
275
294
  errors.push({
276
- field: 'vectorDb.options.url',
295
+ field: 'vectorDb.options.baseUrl',
277
296
  message: 'Weaviate instance URL is required',
278
297
  suggestion: 'Set WEAVIATE_URL environment variable',
279
298
  severity: 'error',
@@ -283,6 +302,10 @@ export class ConfigValidator {
283
302
  return errors;
284
303
  }
285
304
 
305
+ /**
306
+ * Universal REST / custom REST adapter.
307
+ * Options key: { baseUrl: string }
308
+ */
286
309
  private static validateRestConfig(config: VectorDBConfig): ValidationError[] {
287
310
  const errors: ValidationError[] = [];
288
311
  const opts = config.options as Record<string, unknown>;
@@ -320,7 +343,6 @@ export class ConfigValidator {
320
343
  });
321
344
  }
322
345
 
323
- // Provider-specific validation
324
346
  switch (config.provider) {
325
347
  case 'openai':
326
348
  if (!config.apiKey) {
@@ -365,7 +387,6 @@ export class ConfigValidator {
365
387
  break;
366
388
  }
367
389
 
368
- // Validate temperature
369
390
  if (config.temperature !== undefined) {
370
391
  if (config.temperature < 0 || config.temperature > 2) {
371
392
  errors.push({
@@ -376,7 +397,6 @@ export class ConfigValidator {
376
397
  }
377
398
  }
378
399
 
379
- // Validate maxTokens
380
400
  if (config.maxTokens !== undefined && config.maxTokens <= 0) {
381
401
  errors.push({
382
402
  field: 'llm.maxTokens',
@@ -404,12 +424,11 @@ export class ConfigValidator {
404
424
  errors.push({
405
425
  field: 'embedding.model',
406
426
  message: 'Embedding model name is required',
407
- suggestion: 'e.g., "text-embedding-3-small" for OpenAI',
427
+ suggestion: 'e.g., "text-embedding-3-small" for OpenAI, "nomic-embed-text" for Ollama',
408
428
  severity: 'error',
409
429
  });
410
430
  }
411
431
 
412
- // Provider-specific validation
413
432
  if (config.provider === 'openai' && !config.apiKey) {
414
433
  errors.push({
415
434
  field: 'embedding.apiKey',
@@ -422,13 +441,12 @@ export class ConfigValidator {
422
441
  if (config.provider === 'ollama' && !config.baseUrl) {
423
442
  errors.push({
424
443
  field: 'embedding.baseUrl',
425
- message: 'Ollama base URL is required',
444
+ message: 'Ollama base URL is required for embedding',
426
445
  suggestion: 'Set EMBEDDING_BASE_URL environment variable',
427
446
  severity: 'error',
428
447
  });
429
448
  }
430
449
 
431
- // Validate dimensions
432
450
  if (config.dimensions !== undefined && config.dimensions <= 0) {
433
451
  errors.push({
434
452
  field: 'embedding.dimensions',
@@ -526,10 +544,17 @@ export class ConfigValidator {
526
544
  }
527
545
 
528
546
  private static isValidCSSColor(color: string): boolean {
529
- // Check if valid CSS color
530
- const s = new Option().style;
531
- s.color = color;
532
- return s.color !== '';
547
+ const hexRegex = /^#([0-9a-f]{3}){1,2}$/i;
548
+ const rgbRegex = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/i;
549
+ const hslRegex = /^hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)$/i;
550
+ const namedColors = ['red', 'blue', 'green', 'black', 'white', 'transparent'];
551
+
552
+ return (
553
+ hexRegex.test(color) ||
554
+ rgbRegex.test(color) ||
555
+ hslRegex.test(color) ||
556
+ namedColors.includes(color.toLowerCase())
557
+ );
533
558
  }
534
559
 
535
560
  /**
@@ -4,8 +4,7 @@ import { DocumentChunker } from '../rag/DocumentChunker';
4
4
  import { RagConfig } from '../config/RagConfig';
5
5
  import { ProviderRegistry } from './ProviderRegistry';
6
6
  import { BatchProcessor, BatchOptions } from './BatchProcessor';
7
-
8
- // Moved to src/types/index.ts
7
+ import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
9
8
  import { IngestDocument, ChatResponse } from '../types';
10
9
 
11
10
  /**
@@ -14,9 +13,9 @@ import { IngestDocument, ChatResponse } from '../types';
14
13
  * Features:
15
14
  * - Lazy initialization of providers
16
15
  * - Batch processing with retry logic
17
- * - Configurable embedding strategies
16
+ * - Smart embedding strategy (integrated / separate / external)
18
17
  * - Error recovery for transient failures
19
- * - Support for multi-tenancy via namespacing
18
+ * - Multi-tenancy support via namespacing
20
19
  */
21
20
  export class Pipeline {
22
21
  private vectorDB!: BaseVectorProvider;
@@ -37,26 +36,32 @@ export class Pipeline {
37
36
  async initialize(): Promise<void> {
38
37
  if (this.initialised) return;
39
38
 
39
+ // Resolve vector DB provider
40
40
  this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
41
- this.llmProvider = ProviderRegistry.createLLMProvider(this.config.llm, this.config.embedding);
42
-
43
- if (this.config.llm.provider === 'anthropic') {
44
- // Anthropic doesn't support embeddings, so use separate provider
45
- const { LLMFactory } = await import('../llm/LLMFactory');
46
- this.embeddingProvider = LLMFactory.createEmbeddingProvider(this.config.embedding);
47
- } else {
48
- this.embeddingProvider = this.llmProvider;
49
- }
41
+
42
+ // Resolve LLM + embedding providers using EmbeddingStrategyResolver.
43
+ // This handles all cases: integrated (same provider for chat+embed),
44
+ // separate (e.g. Anthropic + OpenAI embedding), and external.
45
+ const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
46
+ this.config.llm,
47
+ this.config.embedding
48
+ );
49
+
50
+ this.llmProvider = llmProvider;
51
+ this.embeddingProvider = embeddingProvider;
50
52
 
51
53
  await this.vectorDB.initialize();
52
54
  this.initialised = true;
53
55
  }
54
56
 
55
57
  /**
56
- * Ingest documents with automatic chunking, embedding, and batch processing.
58
+ * Ingest documents with automatic chunking, embedding, and batch upsert.
57
59
  * Handles retries for transient failures.
58
60
  */
59
- async ingest(documents: IngestDocument[], namespace?: string): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
61
+ async ingest(
62
+ documents: IngestDocument[],
63
+ namespace?: string
64
+ ): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
60
65
  await this.initialize();
61
66
  const ns = namespace ?? this.config.projectId;
62
67
  const results = [];
@@ -68,9 +73,9 @@ export class Pipeline {
68
73
  metadata: doc.metadata,
69
74
  });
70
75
 
71
- // Batch embed chunks with retry logic
72
- const batchOptions: BatchOptions = {
73
- batchSize: 50, // Embedding batch size
76
+ // Batch embed with retry
77
+ const embedBatchOptions: BatchOptions = {
78
+ batchSize: 50,
74
79
  maxRetries: 3,
75
80
  initialDelayMs: 100,
76
81
  };
@@ -78,11 +83,11 @@ export class Pipeline {
78
83
  const vectors = await BatchProcessor.mapWithRetry(
79
84
  chunks.map(c => c.content),
80
85
  (text) => this.embeddingProvider.embed(text),
81
- batchOptions
86
+ embedBatchOptions
82
87
  );
83
88
 
84
89
  if (vectors.length !== chunks.length) {
85
- throw new Error(`Embedding failed: got ${vectors.length} vectors for ${chunks.length} chunks`);
90
+ throw new Error(`Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks`);
86
91
  }
87
92
 
88
93
  const upsertDocs = chunks.map((chunk, i) => ({
@@ -92,7 +97,7 @@ export class Pipeline {
92
97
  metadata: chunk.metadata,
93
98
  }));
94
99
 
95
- // Batch upsert with retry logic
100
+ // Batch upsert with retry
96
101
  const upsertBatchOptions: BatchOptions = {
97
102
  batchSize: 100,
98
103
  maxRetries: 3,
@@ -115,10 +120,7 @@ export class Pipeline {
115
120
  });
116
121
  } catch (error) {
117
122
  console.error(`[Pipeline] Failed to ingest document ${doc.docId}:`, error);
118
- results.push({
119
- docId: doc.docId,
120
- chunksIngested: 0,
121
- });
123
+ results.push({ docId: doc.docId, chunksIngested: 0 });
122
124
  }
123
125
  }
124
126
 
@@ -134,7 +136,7 @@ export class Pipeline {
134
136
  try {
135
137
  const queryVector = await this.embeddingProvider.embed(question);
136
138
  const rawMatches = await this.vectorDB.query(queryVector, topK, ns);
137
-
139
+
138
140
  const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
139
141
  const context = sources.length
140
142
  ? sources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
@@ -0,0 +1,277 @@
1
+ /**
2
+ * PluginManager — Universal plugin registration and lifecycle management
3
+ *
4
+ * Provides a flexible architecture for registering custom vector DBs, LLMs,
5
+ * and other providers without modifying core code.
6
+ *
7
+ * Features:
8
+ * - Dynamic plugin registration
9
+ * - Dependency resolution
10
+ * - Plugin validation
11
+ * - Lifecycle management
12
+ */
13
+
14
+ export interface PluginMetadata {
15
+ /** Unique identifier (e.g., 'myprovider.vectordb.custom') */
16
+ id: string;
17
+ /** Human-readable name */
18
+ name: string;
19
+ /** Plugin version */
20
+ version: string;
21
+ /** Detailed description */
22
+ description?: string;
23
+ /** Author information */
24
+ author?: string;
25
+ /** Supported capabilities */
26
+ capabilities: PluginCapability[];
27
+ /** Required dependencies (other plugin IDs) */
28
+ dependencies?: string[];
29
+ }
30
+
31
+ export type PluginCapability = 'chat' | 'embed' | 'vector-db' | 'reranker' | 'parser';
32
+
33
+ export interface IPluginProvider {
34
+ /**
35
+ * Get plugin metadata
36
+ */
37
+ getMetadata(): PluginMetadata;
38
+
39
+ /**
40
+ * Validate configuration for this plugin
41
+ */
42
+ validateConfig?(config: Record<string, unknown>): {
43
+ valid: boolean;
44
+ errors?: string[];
45
+ };
46
+
47
+ /**
48
+ * Initialize plugin with configuration
49
+ */
50
+ initialize(config: Record<string, unknown>): Promise<void>;
51
+
52
+ /**
53
+ * Check if plugin is ready
54
+ */
55
+ isReady(): Promise<boolean>;
56
+
57
+ /**
58
+ * Gracefully shutdown plugin
59
+ */
60
+ shutdown(): Promise<void>;
61
+
62
+ /**
63
+ * Get plugin-specific health status
64
+ */
65
+ getHealthStatus?(): Promise<{
66
+ healthy: boolean;
67
+ details?: Record<string, unknown>;
68
+ }>;
69
+ }
70
+
71
+ export interface PluginDescriptor {
72
+ metadata: PluginMetadata;
73
+ factory: (config: Record<string, unknown>) => Promise<IPluginProvider>;
74
+ validateConfig?: (config: Record<string, unknown>) => {
75
+ valid: boolean;
76
+ errors?: string[];
77
+ };
78
+ }
79
+
80
+ export class PluginManager {
81
+ private static readonly registeredPlugins: Map<string, PluginDescriptor> = new Map();
82
+ private static readonly instances: Map<string, IPluginProvider> = new Map();
83
+
84
+ /**
85
+ * Register a new plugin provider
86
+ */
87
+ static registerPlugin(descriptor: PluginDescriptor): void {
88
+ const { metadata } = descriptor;
89
+
90
+ if (this.registeredPlugins.has(metadata.id)) {
91
+ throw new Error(
92
+ `[PluginManager] Plugin with ID "${metadata.id}" is already registered. ` +
93
+ `Use unregisterPlugin() first if you need to replace it.`
94
+ );
95
+ }
96
+
97
+ // Validate metadata
98
+ if (!metadata.id || !metadata.name || !metadata.version) {
99
+ throw new Error(
100
+ '[PluginManager] Plugin metadata must include id, name, and version'
101
+ );
102
+ }
103
+
104
+ if (metadata.capabilities.length === 0) {
105
+ throw new Error('[PluginManager] Plugin must declare at least one capability');
106
+ }
107
+
108
+ this.registeredPlugins.set(metadata.id, descriptor);
109
+ console.log(
110
+ `[PluginManager] Registered plugin: ${metadata.name} v${metadata.version} (${metadata.id})`
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Unregister a plugin and cleanup its instance
116
+ */
117
+ static async unregisterPlugin(pluginId: string): Promise<void> {
118
+ if (this.instances.has(pluginId)) {
119
+ const instance = this.instances.get(pluginId)!;
120
+ await instance.shutdown();
121
+ this.instances.delete(pluginId);
122
+ }
123
+
124
+ this.registeredPlugins.delete(pluginId);
125
+ }
126
+
127
+ /**
128
+ * Get plugin metadata
129
+ */
130
+ static getPluginMetadata(pluginId: string): PluginMetadata | null {
131
+ return this.registeredPlugins.get(pluginId)?.metadata ?? null;
132
+ }
133
+
134
+ /**
135
+ * List all registered plugins with optional filtering by capability
136
+ */
137
+ static listPlugins(capability?: PluginCapability): PluginMetadata[] {
138
+ const plugins = Array.from(this.registeredPlugins.values()).map(d => d.metadata);
139
+
140
+ if (capability) {
141
+ return plugins.filter(p => p.capabilities.includes(capability));
142
+ }
143
+
144
+ return plugins;
145
+ }
146
+
147
+ /**
148
+ * Create and cache a plugin instance with dependency resolution
149
+ */
150
+ static async createInstance(
151
+ pluginId: string,
152
+ config: Record<string, unknown>
153
+ ): Promise<IPluginProvider> {
154
+ // Return cached instance if already created
155
+ if (this.instances.has(pluginId)) {
156
+ return this.instances.get(pluginId)!;
157
+ }
158
+
159
+ const descriptor = this.registeredPlugins.get(pluginId);
160
+ if (!descriptor) {
161
+ throw new Error(
162
+ `[PluginManager] Plugin "${pluginId}" not registered. ` +
163
+ `Registered plugins: ${Array.from(this.registeredPlugins.keys()).join(', ')}`
164
+ );
165
+ }
166
+
167
+ const { metadata, factory } = descriptor;
168
+
169
+ // Validate configuration
170
+ if (descriptor.validateConfig) {
171
+ const validation = descriptor.validateConfig(config);
172
+ if (!validation.valid) {
173
+ throw new Error(
174
+ `[PluginManager] Invalid configuration for plugin "${pluginId}": ` +
175
+ `${validation.errors?.join('; ') || 'Unknown error'}`
176
+ );
177
+ }
178
+ }
179
+
180
+ // Resolve dependencies
181
+ if (metadata.dependencies && metadata.dependencies.length > 0) {
182
+ for (const depId of metadata.dependencies) {
183
+ if (!this.instances.has(depId)) {
184
+ throw new Error(
185
+ `[PluginManager] Plugin "${pluginId}" requires "${depId}" to be initialized first`
186
+ );
187
+ }
188
+ }
189
+ }
190
+
191
+ // Initialize plugin
192
+ console.log(`[PluginManager] Initializing plugin: ${pluginId}`);
193
+ const instance = await factory(config);
194
+
195
+ // Verify plugin is ready
196
+ const isReady = await instance.isReady();
197
+ if (!isReady) {
198
+ throw new Error(
199
+ `[PluginManager] Plugin "${pluginId}" failed to initialize properly`
200
+ );
201
+ }
202
+
203
+ // Cache instance
204
+ this.instances.set(pluginId, instance);
205
+
206
+ return instance;
207
+ }
208
+
209
+ /**
210
+ * Get cached instance of a plugin
211
+ */
212
+ static getInstance(pluginId: string): IPluginProvider | null {
213
+ return this.instances.get(pluginId) ?? null;
214
+ }
215
+
216
+ /**
217
+ * Check if a plugin is ready (either registered or instantiated)
218
+ */
219
+ static isPluginAvailable(pluginId: string): boolean {
220
+ return this.registeredPlugins.has(pluginId) || this.instances.has(pluginId);
221
+ }
222
+
223
+ /**
224
+ * Shutdown all plugin instances
225
+ */
226
+ static async shutdownAll(): Promise<void> {
227
+ const errors: Error[] = [];
228
+
229
+ for (const [pluginId, instance] of this.instances.entries()) {
230
+ try {
231
+ await instance.shutdown();
232
+ console.log(`[PluginManager] Shutdown plugin: ${pluginId}`);
233
+ } catch (error) {
234
+ errors.push(
235
+ new Error(`Failed to shutdown plugin ${pluginId}: ${error instanceof Error ? error.message : String(error)}`)
236
+ );
237
+ }
238
+ }
239
+
240
+ this.instances.clear();
241
+
242
+ if (errors.length > 0) {
243
+ throw new AggregateError(errors, '[PluginManager] Errors during shutdown');
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Get health status of all instantiated plugins
249
+ */
250
+ static async getHealthStatus(): Promise<
251
+ Map<string, { healthy: boolean; details?: Record<string, unknown> }>
252
+ > {
253
+ const status = new Map();
254
+
255
+ for (const [pluginId, instance] of this.instances.entries()) {
256
+ try {
257
+ const health = await instance.getHealthStatus?.();
258
+ status.set(pluginId, health ?? { healthy: true });
259
+ } catch (error) {
260
+ status.set(pluginId, {
261
+ healthy: false,
262
+ details: { error: error instanceof Error ? error.message : String(error) },
263
+ });
264
+ }
265
+ }
266
+
267
+ return status;
268
+ }
269
+
270
+ /**
271
+ * Clear all registered and instantiated plugins (for testing)
272
+ */
273
+ static reset(): void {
274
+ this.registeredPlugins.clear();
275
+ this.instances.clear();
276
+ }
277
+ }