@sparkleideas/plugins 3.0.0-alpha.10

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 (80) hide show
  1. package/README.md +401 -0
  2. package/__tests__/collection-manager.test.ts +332 -0
  3. package/__tests__/dependency-graph.test.ts +434 -0
  4. package/__tests__/enhanced-plugin-registry.test.ts +488 -0
  5. package/__tests__/plugin-registry.test.ts +368 -0
  6. package/__tests__/ruvector-bridge.test.ts +2429 -0
  7. package/__tests__/ruvector-integration.test.ts +1602 -0
  8. package/__tests__/ruvector-migrations.test.ts +1099 -0
  9. package/__tests__/ruvector-quantization.test.ts +846 -0
  10. package/__tests__/ruvector-streaming.test.ts +1088 -0
  11. package/__tests__/sdk.test.ts +325 -0
  12. package/__tests__/security.test.ts +348 -0
  13. package/__tests__/utils/ruvector-test-utils.ts +860 -0
  14. package/examples/plugin-creator/index.ts +636 -0
  15. package/examples/plugin-creator/plugin-creator.test.ts +312 -0
  16. package/examples/ruvector/README.md +288 -0
  17. package/examples/ruvector/attention-patterns.ts +394 -0
  18. package/examples/ruvector/basic-usage.ts +288 -0
  19. package/examples/ruvector/docker-compose.yml +75 -0
  20. package/examples/ruvector/gnn-analysis.ts +501 -0
  21. package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
  22. package/examples/ruvector/init-db.sql +119 -0
  23. package/examples/ruvector/quantization.ts +680 -0
  24. package/examples/ruvector/self-learning.ts +447 -0
  25. package/examples/ruvector/semantic-search.ts +576 -0
  26. package/examples/ruvector/streaming-large-data.ts +507 -0
  27. package/examples/ruvector/transactions.ts +594 -0
  28. package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
  29. package/examples/ruvector-plugins/index.ts +79 -0
  30. package/examples/ruvector-plugins/intent-router.ts +354 -0
  31. package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
  32. package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
  33. package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
  34. package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
  35. package/examples/ruvector-plugins/shared/index.ts +20 -0
  36. package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
  37. package/examples/ruvector-plugins/sona-learning.ts +445 -0
  38. package/package.json +97 -0
  39. package/src/collections/collection-manager.ts +661 -0
  40. package/src/collections/index.ts +56 -0
  41. package/src/collections/official/index.ts +1040 -0
  42. package/src/core/base-plugin.ts +416 -0
  43. package/src/core/plugin-interface.ts +215 -0
  44. package/src/hooks/index.ts +685 -0
  45. package/src/index.ts +378 -0
  46. package/src/integrations/agentic-flow.ts +743 -0
  47. package/src/integrations/index.ts +88 -0
  48. package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
  49. package/src/integrations/ruvector/attention-advanced.ts +1040 -0
  50. package/src/integrations/ruvector/attention-executor.ts +782 -0
  51. package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
  52. package/src/integrations/ruvector/attention.ts +1063 -0
  53. package/src/integrations/ruvector/gnn.ts +3050 -0
  54. package/src/integrations/ruvector/hyperbolic.ts +1948 -0
  55. package/src/integrations/ruvector/index.ts +394 -0
  56. package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
  57. package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
  58. package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
  59. package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
  60. package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
  61. package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
  62. package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
  63. package/src/integrations/ruvector/migrations/index.ts +35 -0
  64. package/src/integrations/ruvector/migrations/migrations.ts +647 -0
  65. package/src/integrations/ruvector/quantization.ts +2036 -0
  66. package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
  67. package/src/integrations/ruvector/self-learning.ts +2376 -0
  68. package/src/integrations/ruvector/streaming.ts +1737 -0
  69. package/src/integrations/ruvector/types.ts +1945 -0
  70. package/src/providers/index.ts +643 -0
  71. package/src/registry/dependency-graph.ts +568 -0
  72. package/src/registry/enhanced-plugin-registry.ts +994 -0
  73. package/src/registry/plugin-registry.ts +604 -0
  74. package/src/sdk/index.ts +563 -0
  75. package/src/security/index.ts +594 -0
  76. package/src/types/index.ts +446 -0
  77. package/src/workers/index.ts +700 -0
  78. package/tmp.json +0 -0
  79. package/tsconfig.json +25 -0
  80. package/vitest.config.ts +23 -0
@@ -0,0 +1,2000 @@
1
+ /**
2
+ * RuVector PostgreSQL Bridge Plugin
3
+ *
4
+ * Production-ready plugin for RuVector PostgreSQL integration providing:
5
+ * - Connection management with pooling
6
+ * - Vector similarity search (HNSW, IVF)
7
+ * - Batch operations
8
+ * - Index management
9
+ * - MCP tool integration
10
+ * - Event emission and metrics
11
+ *
12
+ * @module @sparkleideas/plugins/integrations/ruvector
13
+ * @version 1.0.0
14
+ */
15
+
16
+ import { EventEmitter } from 'events';
17
+ import { BasePlugin } from '../../core/base-plugin.js';
18
+ import type { MCPToolDefinition, MCPToolResult, HealthCheckResult } from '../../types/index.js';
19
+ import type {
20
+ RuVectorConfig,
21
+ VectorSearchOptions,
22
+ VectorSearchResult,
23
+ VectorInsertOptions,
24
+ VectorUpdateOptions,
25
+ VectorIndexOptions,
26
+ BatchVectorOptions,
27
+ DistanceMetric,
28
+ VectorIndexType,
29
+ IndexStats,
30
+ QueryResult,
31
+ BatchResult,
32
+ RuVectorEventType,
33
+ ConnectionResult,
34
+ PoolConfig,
35
+ RetryConfig,
36
+ BulkSearchResult,
37
+ HealthStatus,
38
+ RuVectorStats,
39
+ } from './types.js';
40
+
41
+ // ============================================================================
42
+ // Type Definitions for pg (node-postgres)
43
+ // ============================================================================
44
+
45
+ /**
46
+ * PostgreSQL Pool interface (from pg package).
47
+ * Using interface to avoid direct dependency issues.
48
+ */
49
+ interface Pool {
50
+ connect(): Promise<PoolClient>;
51
+ query<T = unknown>(text: string, values?: unknown[]): Promise<PgQueryResult<T>>;
52
+ end(): Promise<void>;
53
+ on(event: string, callback: (...args: unknown[]) => void): this;
54
+ totalCount: number;
55
+ idleCount: number;
56
+ waitingCount: number;
57
+ }
58
+
59
+ interface PoolClient {
60
+ query<T = unknown>(text: string, values?: unknown[]): Promise<PgQueryResult<T>>;
61
+ release(err?: Error): void;
62
+ }
63
+
64
+ interface PgQueryResult<T> {
65
+ rows: T[];
66
+ rowCount: number | null;
67
+ command: string;
68
+ fields?: Array<{ name: string; dataTypeID: number }>;
69
+ }
70
+
71
+ interface PoolFactory {
72
+ Pool: new (config: PgPoolConfig) => Pool;
73
+ }
74
+
75
+ interface PgPoolConfig {
76
+ host: string;
77
+ port: number;
78
+ database: string;
79
+ user: string;
80
+ password: string;
81
+ ssl?: boolean | { rejectUnauthorized?: boolean };
82
+ min?: number;
83
+ max?: number;
84
+ idleTimeoutMillis?: number;
85
+ connectionTimeoutMillis?: number;
86
+ application_name?: string;
87
+ }
88
+
89
+ // ============================================================================
90
+ // Constants
91
+ // ============================================================================
92
+
93
+ const PLUGIN_NAME = 'ruvector-postgres';
94
+ const PLUGIN_VERSION = '1.0.0';
95
+ const DEFAULT_POOL_MIN = 2;
96
+ const DEFAULT_POOL_MAX = 10;
97
+ const DEFAULT_IDLE_TIMEOUT_MS = 30000;
98
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 10000;
99
+ const DEFAULT_QUERY_TIMEOUT_MS = 30000;
100
+ const DEFAULT_VECTOR_COLUMN = 'embedding';
101
+ const DEFAULT_DIMENSIONS = 1536;
102
+ const SLOW_QUERY_THRESHOLD_MS = 1000;
103
+
104
+ /**
105
+ * Distance metric to pgvector operator mapping.
106
+ */
107
+ const DISTANCE_OPERATORS: Record<DistanceMetric, string> = {
108
+ cosine: '<=>',
109
+ euclidean: '<->',
110
+ dot: '<#>',
111
+ hamming: '<~>',
112
+ manhattan: '<+>',
113
+ chebyshev: '<+>', // Not directly supported, fallback
114
+ jaccard: '<~>', // Binary similarity
115
+ minkowski: '<->', // Fallback to L2
116
+ bray_curtis: '<->', // Fallback
117
+ canberra: '<->', // Fallback
118
+ mahalanobis: '<->', // Fallback
119
+ correlation: '<=>', // Similar to cosine
120
+ };
121
+
122
+ /**
123
+ * Index type to SQL mapping.
124
+ */
125
+ const INDEX_TYPE_SQL: Record<VectorIndexType, string> = {
126
+ hnsw: 'hnsw',
127
+ ivfflat: 'ivfflat',
128
+ ivfpq: 'ivfflat', // IVF with PQ uses similar syntax
129
+ flat: '', // No index (brute force)
130
+ diskann: 'hnsw', // Fallback to HNSW
131
+ };
132
+
133
+ // ============================================================================
134
+ // Metrics Interface
135
+ // ============================================================================
136
+
137
+ /**
138
+ * Metrics collected by the RuVector Bridge.
139
+ */
140
+ interface RuVectorMetrics {
141
+ queriesTotal: number;
142
+ queriesSucceeded: number;
143
+ queriesFailed: number;
144
+ slowQueries: number;
145
+ avgQueryTimeMs: number;
146
+ vectorsInserted: number;
147
+ vectorsUpdated: number;
148
+ vectorsDeleted: number;
149
+ searchesPerformed: number;
150
+ cacheHits: number;
151
+ cacheMisses: number;
152
+ connectionAcquires: number;
153
+ connectionReleases: number;
154
+ connectionErrors: number;
155
+ lastQueryTime: number;
156
+ uptime: number;
157
+ }
158
+
159
+ // ============================================================================
160
+ // Connection Manager
161
+ // ============================================================================
162
+
163
+ /**
164
+ * Manages PostgreSQL connection pooling with automatic retry and health monitoring.
165
+ */
166
+ class ConnectionManager extends EventEmitter {
167
+ private pool: Pool | null = null;
168
+ private readonly config: RuVectorConfig;
169
+ private readonly retryConfig: RetryConfig;
170
+ private connectionId = 0;
171
+ private isConnected = false;
172
+ private lastHealthCheck: Date | null = null;
173
+
174
+ constructor(config: RuVectorConfig) {
175
+ super();
176
+ this.config = config;
177
+ this.retryConfig = config.retry ?? {
178
+ maxAttempts: 3,
179
+ initialDelayMs: 1000,
180
+ maxDelayMs: 30000,
181
+ backoffMultiplier: 2,
182
+ jitter: true,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Initialize the connection pool.
188
+ */
189
+ async initialize(): Promise<ConnectionResult> {
190
+ if (this.pool) {
191
+ throw new Error('Connection pool already initialized');
192
+ }
193
+
194
+ const poolConfig = this.buildPoolConfig();
195
+
196
+ try {
197
+ // Dynamically import pg to avoid bundling issues
198
+ const pg = await this.loadPg();
199
+ this.pool = new pg.Pool(poolConfig);
200
+
201
+ // Set up event handlers
202
+ this.pool.on('connect', () => {
203
+ this.connectionId++;
204
+ this.emit('connection:open', {
205
+ connectionId: `conn-${this.connectionId}`,
206
+ host: this.config.host,
207
+ port: this.config.port,
208
+ database: this.config.database,
209
+ });
210
+ });
211
+
212
+ this.pool.on('error', (...args: unknown[]) => {
213
+ const err = args[0] as Error;
214
+ this.emit('connection:error', {
215
+ error: err,
216
+ code: (err as { code?: string }).code,
217
+ });
218
+ });
219
+
220
+ // Test connection
221
+ const client = await this.pool.connect();
222
+ const result = await client.query<{ version: string; ruvector_version?: string }>(
223
+ "SELECT version() as version, COALESCE(ruvector.version(), 'N/A') as ruvector_version"
224
+ );
225
+ client.release();
226
+
227
+ this.isConnected = true;
228
+ this.lastHealthCheck = new Date();
229
+
230
+ const connectionResult: ConnectionResult = {
231
+ connectionId: `conn-${this.connectionId}`,
232
+ ready: true,
233
+ serverVersion: result.rows[0]?.version ?? 'unknown',
234
+ ruVectorVersion: result.rows[0]?.ruvector_version ?? 'N/A',
235
+ parameters: {
236
+ host: this.config.host,
237
+ port: String(this.config.port),
238
+ database: this.config.database,
239
+ ssl: String(!!this.config.ssl),
240
+ },
241
+ };
242
+
243
+ return connectionResult;
244
+ } catch (error) {
245
+ this.isConnected = false;
246
+ throw new Error(`Failed to initialize connection pool: ${(error as Error).message}`);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Load pg module dynamically.
252
+ */
253
+ private async loadPg(): Promise<PoolFactory> {
254
+ try {
255
+ // Try to import pg
256
+ const pg = await import('pg');
257
+ return pg.default ?? pg;
258
+ } catch {
259
+ throw new Error(
260
+ 'pg (node-postgres) package not found. Install it with: npm install pg'
261
+ );
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Build pool configuration from RuVector config.
267
+ */
268
+ private buildPoolConfig(): PgPoolConfig {
269
+ const poolSettings = (this.config.pool ?? {}) as Partial<PoolConfig>;
270
+
271
+ return {
272
+ host: this.config.host,
273
+ port: this.config.port,
274
+ database: this.config.database,
275
+ user: this.config.user,
276
+ password: this.config.password,
277
+ ssl: this.config.ssl
278
+ ? typeof this.config.ssl === 'boolean'
279
+ ? { rejectUnauthorized: false }
280
+ : { rejectUnauthorized: this.config.ssl.rejectUnauthorized ?? true }
281
+ : undefined,
282
+ min: poolSettings.min ?? DEFAULT_POOL_MIN,
283
+ max: poolSettings.max ?? this.config.poolSize ?? DEFAULT_POOL_MAX,
284
+ idleTimeoutMillis: poolSettings.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
285
+ connectionTimeoutMillis: this.config.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS,
286
+ application_name: this.config.applicationName ?? 'claude-flow-ruvector',
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Execute a query with timeout and retry logic.
292
+ */
293
+ async query<T = Record<string, unknown>>(
294
+ sql: string,
295
+ params?: unknown[],
296
+ timeoutMs?: number
297
+ ): Promise<QueryResult<T>> {
298
+ if (!this.pool) {
299
+ throw new Error('Connection pool not initialized');
300
+ }
301
+
302
+ const startTime = Date.now();
303
+ const queryId = `query-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
304
+ const timeout = timeoutMs ?? this.config.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
305
+
306
+ this.emit('query:start', { queryId, sql, params });
307
+
308
+ let lastError: Error | null = null;
309
+ let attempt = 0;
310
+
311
+ while (attempt < this.retryConfig.maxAttempts) {
312
+ attempt++;
313
+
314
+ try {
315
+ const result = await this.executeWithTimeout<T>(sql, params, timeout);
316
+ const durationMs = Date.now() - startTime;
317
+
318
+ const queryResult: QueryResult<T> = {
319
+ rows: result.rows,
320
+ rowCount: result.rowCount ?? 0,
321
+ affectedRows: result.rowCount ?? undefined,
322
+ durationMs,
323
+ command: result.command,
324
+ };
325
+
326
+ this.emit('query:complete', {
327
+ queryId,
328
+ durationMs,
329
+ rowCount: queryResult.rowCount,
330
+ });
331
+
332
+ if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
333
+ this.emit('query:slow', {
334
+ queryId,
335
+ durationMs,
336
+ rowCount: queryResult.rowCount,
337
+ threshold: SLOW_QUERY_THRESHOLD_MS,
338
+ });
339
+ }
340
+
341
+ return queryResult;
342
+ } catch (error) {
343
+ lastError = error as Error;
344
+ const isRetryable = this.isRetryableError(lastError);
345
+
346
+ if (!isRetryable || attempt >= this.retryConfig.maxAttempts) {
347
+ break;
348
+ }
349
+
350
+ // Calculate delay with exponential backoff and optional jitter
351
+ let delay = this.retryConfig.initialDelayMs * Math.pow(this.retryConfig.backoffMultiplier, attempt - 1);
352
+ delay = Math.min(delay, this.retryConfig.maxDelayMs);
353
+
354
+ if (this.retryConfig.jitter) {
355
+ delay = delay * (0.5 + Math.random());
356
+ }
357
+
358
+ await this.sleep(delay);
359
+ }
360
+ }
361
+
362
+ const durationMs = Date.now() - startTime;
363
+ this.emit('query:error', {
364
+ queryId,
365
+ sql,
366
+ params,
367
+ error: lastError!,
368
+ durationMs,
369
+ });
370
+
371
+ throw lastError!;
372
+ }
373
+
374
+ /**
375
+ * Execute query with timeout.
376
+ */
377
+ private async executeWithTimeout<T>(
378
+ sql: string,
379
+ params: unknown[] | undefined,
380
+ timeoutMs: number
381
+ ): Promise<PgQueryResult<T>> {
382
+ return new Promise(async (resolve, reject) => {
383
+ const timer = setTimeout(() => {
384
+ reject(new Error(`Query timed out after ${timeoutMs}ms`));
385
+ }, timeoutMs);
386
+
387
+ try {
388
+ const result = await this.pool!.query<T>(sql, params);
389
+ clearTimeout(timer);
390
+ resolve(result);
391
+ } catch (error) {
392
+ clearTimeout(timer);
393
+ reject(error);
394
+ }
395
+ });
396
+ }
397
+
398
+ /**
399
+ * Check if error is retryable.
400
+ */
401
+ private isRetryableError(error: Error): boolean {
402
+ const code = (error as { code?: string }).code;
403
+ const retryableCodes = this.retryConfig.retryableErrors ?? [
404
+ 'ECONNREFUSED',
405
+ 'ECONNRESET',
406
+ 'ETIMEDOUT',
407
+ '57P01', // admin_shutdown
408
+ '57P02', // crash_shutdown
409
+ '57P03', // cannot_connect_now
410
+ '40001', // serialization_failure
411
+ '40P01', // deadlock_detected
412
+ ];
413
+ return code !== undefined && retryableCodes.includes(code);
414
+ }
415
+
416
+ /**
417
+ * Get a client from the pool.
418
+ */
419
+ async getClient(): Promise<PoolClient> {
420
+ if (!this.pool) {
421
+ throw new Error('Connection pool not initialized');
422
+ }
423
+
424
+ this.emit('connection:pool_acquired', this.getPoolStats());
425
+ return this.pool.connect();
426
+ }
427
+
428
+ /**
429
+ * Release a client back to the pool.
430
+ */
431
+ releaseClient(client: PoolClient, error?: Error): void {
432
+ client.release(error);
433
+ this.emit('connection:pool_released', this.getPoolStats());
434
+ }
435
+
436
+ /**
437
+ * Get pool statistics.
438
+ */
439
+ getPoolStats(): {
440
+ connectionId: string;
441
+ poolSize: number;
442
+ availableConnections: number;
443
+ waitingClients: number;
444
+ } {
445
+ return {
446
+ connectionId: `pool-${this.connectionId}`,
447
+ poolSize: this.pool?.totalCount ?? 0,
448
+ availableConnections: this.pool?.idleCount ?? 0,
449
+ waitingClients: this.pool?.waitingCount ?? 0,
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Check if connected.
455
+ */
456
+ isHealthy(): boolean {
457
+ return this.isConnected && this.pool !== null;
458
+ }
459
+
460
+ /**
461
+ * Shutdown the connection pool.
462
+ */
463
+ async shutdown(): Promise<void> {
464
+ if (this.pool) {
465
+ await this.pool.end();
466
+ this.pool = null;
467
+ this.isConnected = false;
468
+ this.emit('connection:close', {
469
+ connectionId: `conn-${this.connectionId}`,
470
+ host: this.config.host,
471
+ port: this.config.port,
472
+ database: this.config.database,
473
+ });
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Sleep utility.
479
+ */
480
+ private sleep(ms: number): Promise<void> {
481
+ return new Promise(resolve => setTimeout(resolve, ms));
482
+ }
483
+ }
484
+
485
+ // ============================================================================
486
+ // Vector Operations
487
+ // ============================================================================
488
+
489
+ /**
490
+ * Provides vector operation methods for search, insert, update, and delete.
491
+ */
492
+ class VectorOps {
493
+ private readonly connectionManager: ConnectionManager;
494
+ private readonly config: RuVectorConfig;
495
+
496
+ constructor(connectionManager: ConnectionManager, config: RuVectorConfig) {
497
+ this.connectionManager = connectionManager;
498
+ this.config = config;
499
+ }
500
+
501
+ /**
502
+ * Perform vector similarity search.
503
+ */
504
+ async search(options: VectorSearchOptions): Promise<VectorSearchResult[]> {
505
+ const tableName = options.tableName ?? 'vectors';
506
+ const vectorColumn = options.vectorColumn ?? DEFAULT_VECTOR_COLUMN;
507
+ const metric = options.metric ?? 'cosine';
508
+ const operator = DISTANCE_OPERATORS[metric] ?? '<=>';
509
+
510
+ // Build query vector string
511
+ const queryVector = this.formatVector(options.query);
512
+
513
+ // Set HNSW parameters if specified
514
+ if (options.efSearch) {
515
+ await this.connectionManager.query(
516
+ `SET LOCAL hnsw.ef_search = ${options.efSearch}`
517
+ );
518
+ }
519
+
520
+ // Set IVF probes if specified
521
+ if (options.probes) {
522
+ await this.connectionManager.query(
523
+ `SET LOCAL ivfflat.probes = ${options.probes}`
524
+ );
525
+ }
526
+
527
+ // Build SELECT clause
528
+ const selectColumns = options.selectColumns ?? ['id'];
529
+ const columnList = [...selectColumns];
530
+
531
+ if (options.includeVector) {
532
+ columnList.push(vectorColumn);
533
+ }
534
+ if (options.includeMetadata) {
535
+ columnList.push('metadata');
536
+ }
537
+
538
+ // Add distance/similarity calculation
539
+ const distanceExpr = `${vectorColumn} ${operator} '${queryVector}'::vector`;
540
+ columnList.push(`(${distanceExpr}) as distance`);
541
+
542
+ // Build WHERE clause
543
+ const whereClauses: string[] = [];
544
+ const params: unknown[] = [];
545
+ let paramIndex = 1;
546
+
547
+ if (options.threshold !== undefined) {
548
+ if (metric === 'cosine' || metric === 'dot') {
549
+ // For similarity metrics, higher is better
550
+ whereClauses.push(`(1 - (${distanceExpr})) >= $${paramIndex++}`);
551
+ params.push(options.threshold);
552
+ } else {
553
+ // For distance metrics, lower is better
554
+ whereClauses.push(`(${distanceExpr}) <= $${paramIndex++}`);
555
+ params.push(options.threshold);
556
+ }
557
+ }
558
+
559
+ if (options.maxDistance !== undefined) {
560
+ whereClauses.push(`(${distanceExpr}) <= $${paramIndex++}`);
561
+ params.push(options.maxDistance);
562
+ }
563
+
564
+ if (options.filter) {
565
+ for (const [key, value] of Object.entries(options.filter)) {
566
+ if (key === 'metadata') {
567
+ // JSONB containment
568
+ whereClauses.push(`metadata @> $${paramIndex++}::jsonb`);
569
+ params.push(JSON.stringify(value));
570
+ } else {
571
+ whereClauses.push(`${this.escapeIdentifier(key)} = $${paramIndex++}`);
572
+ params.push(value);
573
+ }
574
+ }
575
+ }
576
+
577
+ if (options.whereClause) {
578
+ whereClauses.push(`(${options.whereClause})`);
579
+ if (options.whereParams) {
580
+ // Re-index parameters in the custom WHERE clause
581
+ const reindexed = options.whereParams.map(() => `$${paramIndex++}`);
582
+ params.push(...options.whereParams);
583
+ }
584
+ }
585
+
586
+ // Build final query
587
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
588
+ let sql = `SELECT ${columnList.join(', ')} FROM ${schemaPrefix}${this.escapeIdentifier(tableName)}`;
589
+
590
+ if (whereClauses.length > 0) {
591
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
592
+ }
593
+
594
+ sql += ` ORDER BY ${distanceExpr} ASC`;
595
+ sql += ` LIMIT ${options.k}`;
596
+
597
+ const result = await this.connectionManager.query<{
598
+ id: string | number;
599
+ distance: number;
600
+ [key: string]: unknown;
601
+ }>(sql, params, options.timeoutMs);
602
+
603
+ // Transform results
604
+ return result.rows.map((row, index) => {
605
+ const score = metric === 'cosine' || metric === 'dot'
606
+ ? 1 - (row.distance as number)
607
+ : 1 / (1 + (row.distance as number));
608
+
609
+ const searchResult: VectorSearchResult = {
610
+ id: row.id,
611
+ score,
612
+ distance: row.distance as number,
613
+ rank: index + 1,
614
+ retrievedAt: new Date(),
615
+ };
616
+
617
+ if (options.includeVector && row[vectorColumn]) {
618
+ (searchResult as { vector?: number[] }).vector = this.parseVector(row[vectorColumn] as string);
619
+ }
620
+
621
+ if (options.includeMetadata && row.metadata) {
622
+ (searchResult as { metadata?: Record<string, unknown> }).metadata = row.metadata as Record<string, unknown>;
623
+ }
624
+
625
+ return searchResult;
626
+ });
627
+ }
628
+
629
+ /**
630
+ * Perform batch vector search.
631
+ */
632
+ async batchSearch(options: BatchVectorOptions): Promise<BulkSearchResult> {
633
+ const startTime = Date.now();
634
+ const results: VectorSearchResult[][] = [];
635
+ let cacheHits = 0;
636
+ let cacheMisses = 0;
637
+
638
+ const concurrency = options.concurrency ?? 4;
639
+ const queries = options.queries;
640
+
641
+ // Process queries in parallel batches
642
+ for (let i = 0; i < queries.length; i += concurrency) {
643
+ const batch = queries.slice(i, i + concurrency);
644
+ const batchResults = await Promise.all(
645
+ batch.map(query =>
646
+ this.search({
647
+ query,
648
+ k: options.k,
649
+ metric: options.metric,
650
+ filter: options.filter,
651
+ tableName: options.tableName,
652
+ vectorColumn: options.vectorColumn,
653
+ })
654
+ )
655
+ );
656
+ results.push(...batchResults);
657
+ cacheMisses += batch.length; // No caching implemented yet
658
+ }
659
+
660
+ const totalDurationMs = Date.now() - startTime;
661
+
662
+ return {
663
+ results,
664
+ totalDurationMs,
665
+ avgDurationMs: totalDurationMs / queries.length,
666
+ cacheStats: {
667
+ hits: cacheHits,
668
+ misses: cacheMisses,
669
+ hitRate: cacheHits / (cacheHits + cacheMisses) || 0,
670
+ },
671
+ };
672
+ }
673
+
674
+ /**
675
+ * Insert vectors.
676
+ */
677
+ async insert(options: VectorInsertOptions): Promise<BatchResult<string>> {
678
+ const startTime = Date.now();
679
+ const tableName = options.tableName;
680
+ const vectorColumn = options.vectorColumn ?? DEFAULT_VECTOR_COLUMN;
681
+ const batchSize = options.batchSize ?? 100;
682
+
683
+ const successful: string[] = [];
684
+ const errors: Array<{ index: number; message: string; input?: unknown }> = [];
685
+ let insertedCount = 0;
686
+
687
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
688
+
689
+ // Process in batches
690
+ for (let i = 0; i < options.vectors.length; i += batchSize) {
691
+ const batch = options.vectors.slice(i, i + batchSize);
692
+
693
+ try {
694
+ // Build multi-row INSERT
695
+ const values: string[] = [];
696
+ const params: unknown[] = [];
697
+ let paramIndex = 1;
698
+
699
+ for (const item of batch) {
700
+ const vector = this.formatVector(item.vector);
701
+ const metadata = item.metadata ? JSON.stringify(item.metadata) : null;
702
+
703
+ if (item.id !== undefined) {
704
+ values.push(`($${paramIndex++}, '${vector}'::vector, $${paramIndex++}::jsonb)`);
705
+ params.push(item.id, metadata);
706
+ } else {
707
+ values.push(`(gen_random_uuid(), '${vector}'::vector, $${paramIndex++}::jsonb)`);
708
+ params.push(metadata);
709
+ }
710
+ }
711
+
712
+ let sql = `INSERT INTO ${schemaPrefix}${this.escapeIdentifier(tableName)} `;
713
+ sql += `(id, ${this.escapeIdentifier(vectorColumn)}, metadata) VALUES ${values.join(', ')}`;
714
+
715
+ if (options.upsert) {
716
+ const conflictCols = options.conflictColumns ?? ['id'];
717
+ sql += ` ON CONFLICT (${conflictCols.join(', ')}) DO UPDATE SET `;
718
+ sql += `${this.escapeIdentifier(vectorColumn)} = EXCLUDED.${this.escapeIdentifier(vectorColumn)}, `;
719
+ sql += `metadata = EXCLUDED.metadata`;
720
+ }
721
+
722
+ if (options.returning) {
723
+ sql += ' RETURNING id';
724
+ }
725
+
726
+ const result = await this.connectionManager.query<{ id: string }>(sql, params);
727
+
728
+ if (options.returning && result.rows) {
729
+ successful.push(...result.rows.map(r => String(r.id)));
730
+ }
731
+
732
+ insertedCount += batch.length;
733
+ } catch (error) {
734
+ if (options.skipInvalid) {
735
+ // Try inserting individually
736
+ for (let j = 0; j < batch.length; j++) {
737
+ try {
738
+ const item = batch[j];
739
+ const vector = this.formatVector(item.vector);
740
+ const metadata = item.metadata ? JSON.stringify(item.metadata) : null;
741
+
742
+ const sql = `INSERT INTO ${schemaPrefix}${this.escapeIdentifier(tableName)} ` +
743
+ `(id, ${this.escapeIdentifier(vectorColumn)}, metadata) VALUES ` +
744
+ `($1, '${vector}'::vector, $2::jsonb)` +
745
+ (options.returning ? ' RETURNING id' : '');
746
+
747
+ const result = await this.connectionManager.query<{ id: string }>(
748
+ sql,
749
+ [item.id ?? null, metadata]
750
+ );
751
+
752
+ if (options.returning && result.rows.length > 0) {
753
+ successful.push(String(result.rows[0].id));
754
+ }
755
+ insertedCount++;
756
+ } catch (itemError) {
757
+ errors.push({
758
+ index: i + j,
759
+ message: (itemError as Error).message,
760
+ input: batch[j],
761
+ });
762
+ }
763
+ }
764
+ } else {
765
+ errors.push({
766
+ index: i,
767
+ message: (error as Error).message,
768
+ });
769
+ break;
770
+ }
771
+ }
772
+ }
773
+
774
+ const durationMs = Date.now() - startTime;
775
+
776
+ return {
777
+ total: options.vectors.length,
778
+ successful: insertedCount,
779
+ failed: options.vectors.length - insertedCount,
780
+ results: options.returning ? successful : undefined,
781
+ errors: errors.length > 0 ? errors : undefined,
782
+ durationMs,
783
+ throughput: insertedCount / (durationMs / 1000),
784
+ };
785
+ }
786
+
787
+ /**
788
+ * Update a vector.
789
+ */
790
+ async update(options: VectorUpdateOptions): Promise<boolean> {
791
+ const tableName = options.tableName;
792
+ const vectorColumn = options.vectorColumn ?? DEFAULT_VECTOR_COLUMN;
793
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
794
+
795
+ const setClauses: string[] = [];
796
+ const params: unknown[] = [];
797
+ let paramIndex = 1;
798
+
799
+ if (options.vector) {
800
+ const vector = this.formatVector(options.vector);
801
+ setClauses.push(`${this.escapeIdentifier(vectorColumn)} = '${vector}'::vector`);
802
+ }
803
+
804
+ if (options.metadata) {
805
+ if (options.mergeMetadata) {
806
+ setClauses.push(`metadata = metadata || $${paramIndex++}::jsonb`);
807
+ } else {
808
+ setClauses.push(`metadata = $${paramIndex++}::jsonb`);
809
+ }
810
+ params.push(JSON.stringify(options.metadata));
811
+ }
812
+
813
+ if (setClauses.length === 0) {
814
+ return false;
815
+ }
816
+
817
+ params.push(options.id);
818
+ const sql = `UPDATE ${schemaPrefix}${this.escapeIdentifier(tableName)} ` +
819
+ `SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`;
820
+
821
+ const result = await this.connectionManager.query(sql, params);
822
+ return (result.affectedRows ?? 0) > 0;
823
+ }
824
+
825
+ /**
826
+ * Delete a vector.
827
+ */
828
+ async delete(tableName: string, id: string | number): Promise<boolean> {
829
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
830
+ const sql = `DELETE FROM ${schemaPrefix}${this.escapeIdentifier(tableName)} WHERE id = $1`;
831
+ const result = await this.connectionManager.query(sql, [id]);
832
+ return (result.affectedRows ?? 0) > 0;
833
+ }
834
+
835
+ /**
836
+ * Bulk delete vectors.
837
+ */
838
+ async bulkDelete(tableName: string, ids: Array<string | number>): Promise<BatchResult> {
839
+ const startTime = Date.now();
840
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
841
+
842
+ const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');
843
+ const sql = `DELETE FROM ${schemaPrefix}${this.escapeIdentifier(tableName)} WHERE id IN (${placeholders})`;
844
+
845
+ const result = await this.connectionManager.query(sql, ids);
846
+ const durationMs = Date.now() - startTime;
847
+ const deleted = result.affectedRows ?? 0;
848
+
849
+ return {
850
+ total: ids.length,
851
+ successful: deleted,
852
+ failed: ids.length - deleted,
853
+ durationMs,
854
+ throughput: deleted / (durationMs / 1000),
855
+ };
856
+ }
857
+
858
+ /**
859
+ * Create a vector index.
860
+ */
861
+ async createIndex(options: VectorIndexOptions): Promise<void> {
862
+ const indexType = INDEX_TYPE_SQL[options.indexType];
863
+ if (!indexType && options.indexType !== 'flat') {
864
+ throw new Error(`Unsupported index type: ${options.indexType}`);
865
+ }
866
+
867
+ const indexName = options.indexName ??
868
+ `idx_${options.tableName}_${options.columnName}_${options.indexType}`;
869
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
870
+
871
+ if (options.replace) {
872
+ await this.connectionManager.query(
873
+ `DROP INDEX IF EXISTS ${schemaPrefix}${this.escapeIdentifier(indexName)}`
874
+ );
875
+ }
876
+
877
+ if (options.indexType === 'flat') {
878
+ return; // No index needed for brute force
879
+ }
880
+
881
+ // Build operator class based on metric
882
+ const opClass = this.getOperatorClass(options.metric ?? 'cosine', options.indexType);
883
+
884
+ // Build WITH clause for index parameters
885
+ const withParams: string[] = [];
886
+ if (options.m !== undefined) {
887
+ withParams.push(`m = ${options.m}`);
888
+ }
889
+ if (options.efConstruction !== undefined) {
890
+ withParams.push(`ef_construction = ${options.efConstruction}`);
891
+ }
892
+ if (options.lists !== undefined) {
893
+ withParams.push(`lists = ${options.lists}`);
894
+ }
895
+
896
+ const withClause = withParams.length > 0 ? ` WITH (${withParams.join(', ')})` : '';
897
+ const concurrent = options.concurrent ? 'CONCURRENTLY ' : '';
898
+
899
+ const sql = `CREATE INDEX ${concurrent}${this.escapeIdentifier(indexName)} ` +
900
+ `ON ${schemaPrefix}${this.escapeIdentifier(options.tableName)} ` +
901
+ `USING ${indexType} (${this.escapeIdentifier(options.columnName)} ${opClass})${withClause}`;
902
+
903
+ await this.connectionManager.query(sql);
904
+ }
905
+
906
+ /**
907
+ * Drop an index.
908
+ */
909
+ async dropIndex(indexName: string): Promise<void> {
910
+ const schemaPrefix = this.config.schema ? `${this.escapeIdentifier(this.config.schema)}.` : '';
911
+ await this.connectionManager.query(
912
+ `DROP INDEX IF EXISTS ${schemaPrefix}${this.escapeIdentifier(indexName)}`
913
+ );
914
+ }
915
+
916
+ /**
917
+ * Rebuild an index.
918
+ */
919
+ async rebuildIndex(indexName: string): Promise<void> {
920
+ await this.connectionManager.query(`REINDEX INDEX ${this.escapeIdentifier(indexName)}`);
921
+ }
922
+
923
+ /**
924
+ * Get index statistics.
925
+ */
926
+ async getIndexStats(indexName: string): Promise<IndexStats> {
927
+ const result = await this.connectionManager.query<{
928
+ indexrelname: string;
929
+ idx_scan: number;
930
+ idx_tup_read: number;
931
+ idx_tup_fetch: number;
932
+ pg_relation_size: number;
933
+ }>(
934
+ `SELECT
935
+ indexrelname,
936
+ idx_scan,
937
+ idx_tup_read,
938
+ idx_tup_fetch,
939
+ pg_relation_size(indexrelid) as pg_relation_size
940
+ FROM pg_stat_user_indexes
941
+ WHERE indexrelname = $1`,
942
+ [indexName]
943
+ );
944
+
945
+ if (result.rows.length === 0) {
946
+ throw new Error(`Index ${indexName} not found`);
947
+ }
948
+
949
+ const row = result.rows[0];
950
+ return {
951
+ indexName: row.indexrelname,
952
+ indexType: 'hnsw', // Would need additional query to determine
953
+ numVectors: row.idx_tup_read,
954
+ sizeBytes: row.pg_relation_size,
955
+ buildTimeMs: 0, // Not available from stats
956
+ lastRebuild: new Date(),
957
+ params: {
958
+ scans: row.idx_scan,
959
+ tuplesRead: row.idx_tup_read,
960
+ tuplesFetched: row.idx_tup_fetch,
961
+ },
962
+ };
963
+ }
964
+
965
+ /**
966
+ * List all indices for a table.
967
+ */
968
+ async listIndices(tableName?: string): Promise<IndexStats[]> {
969
+ let sql = `SELECT
970
+ indexrelname,
971
+ idx_scan,
972
+ idx_tup_read,
973
+ idx_tup_fetch,
974
+ pg_relation_size(indexrelid) as pg_relation_size
975
+ FROM pg_stat_user_indexes`;
976
+
977
+ const params: unknown[] = [];
978
+ if (tableName) {
979
+ sql += ` WHERE relname = $1`;
980
+ params.push(tableName);
981
+ }
982
+
983
+ const result = await this.connectionManager.query<{
984
+ indexrelname: string;
985
+ idx_scan: number;
986
+ idx_tup_read: number;
987
+ idx_tup_fetch: number;
988
+ pg_relation_size: number;
989
+ }>(sql, params);
990
+
991
+ return result.rows.map(row => ({
992
+ indexName: row.indexrelname,
993
+ indexType: 'hnsw' as VectorIndexType,
994
+ numVectors: row.idx_tup_read,
995
+ sizeBytes: row.pg_relation_size,
996
+ buildTimeMs: 0,
997
+ lastRebuild: new Date(),
998
+ params: {
999
+ scans: row.idx_scan,
1000
+ tuplesRead: row.idx_tup_read,
1001
+ tuplesFetched: row.idx_tup_fetch,
1002
+ },
1003
+ }));
1004
+ }
1005
+
1006
+ /**
1007
+ * Get operator class for index creation.
1008
+ */
1009
+ private getOperatorClass(metric: DistanceMetric, indexType: VectorIndexType): string {
1010
+ const opClasses: Record<string, Record<string, string>> = {
1011
+ hnsw: {
1012
+ cosine: 'vector_cosine_ops',
1013
+ euclidean: 'vector_l2_ops',
1014
+ dot: 'vector_ip_ops',
1015
+ },
1016
+ ivfflat: {
1017
+ cosine: 'vector_cosine_ops',
1018
+ euclidean: 'vector_l2_ops',
1019
+ dot: 'vector_ip_ops',
1020
+ },
1021
+ };
1022
+
1023
+ return opClasses[indexType]?.[metric] ?? 'vector_cosine_ops';
1024
+ }
1025
+
1026
+ /**
1027
+ * Format vector for SQL.
1028
+ */
1029
+ private formatVector(vector: number[] | Float32Array): string {
1030
+ const arr = Array.isArray(vector) ? vector : Array.from(vector);
1031
+ return `[${arr.join(',')}]`;
1032
+ }
1033
+
1034
+ /**
1035
+ * Parse vector from SQL result.
1036
+ */
1037
+ private parseVector(vectorStr: string): number[] {
1038
+ // Handle pgvector format: [1,2,3] or {1,2,3}
1039
+ const cleaned = vectorStr.replace(/[\[\]{}]/g, '');
1040
+ return cleaned.split(',').map(Number);
1041
+ }
1042
+
1043
+ /**
1044
+ * Escape SQL identifier.
1045
+ */
1046
+ private escapeIdentifier(identifier: string): string {
1047
+ // Basic SQL injection prevention
1048
+ return `"${identifier.replace(/"/g, '""')}"`;
1049
+ }
1050
+ }
1051
+
1052
+ // ============================================================================
1053
+ // RuVector Bridge Plugin
1054
+ // ============================================================================
1055
+
1056
+ /**
1057
+ * RuVector PostgreSQL Bridge Plugin for Claude-Flow v3.
1058
+ *
1059
+ * Provides comprehensive vector database integration with:
1060
+ * - Connection pooling and management
1061
+ * - Vector similarity search (HNSW, IVF)
1062
+ * - Batch operations for high throughput
1063
+ * - Index creation and management
1064
+ * - MCP tool integration
1065
+ * - Event-driven architecture
1066
+ * - Production-ready error handling and metrics
1067
+ *
1068
+ * @example
1069
+ * ```typescript
1070
+ * const bridge = new RuVectorBridge({
1071
+ * host: 'localhost',
1072
+ * port: 5432,
1073
+ * database: 'vectors',
1074
+ * user: 'postgres',
1075
+ * password: 'password',
1076
+ * poolSize: 10,
1077
+ * });
1078
+ *
1079
+ * await bridge.initialize(context);
1080
+ *
1081
+ * const results = await bridge.vectorSearch({
1082
+ * query: [0.1, 0.2, 0.3, ...],
1083
+ * k: 10,
1084
+ * metric: 'cosine',
1085
+ * tableName: 'embeddings',
1086
+ * });
1087
+ * ```
1088
+ */
1089
+ export class RuVectorBridge extends BasePlugin {
1090
+ private readonly ruVectorConfig: RuVectorConfig;
1091
+ private connectionManager: ConnectionManager | null = null;
1092
+ private vectorOps: VectorOps | null = null;
1093
+ private metrics: RuVectorMetrics;
1094
+ private initTime: Date | null = null;
1095
+
1096
+ constructor(config: RuVectorConfig) {
1097
+ super({
1098
+ name: PLUGIN_NAME,
1099
+ version: PLUGIN_VERSION,
1100
+ description: 'RuVector PostgreSQL Bridge for Claude-Flow v3 - Advanced vector database integration',
1101
+ tags: ['database', 'vector', 'postgresql', 'search', 'embeddings'],
1102
+ });
1103
+
1104
+ this.ruVectorConfig = config;
1105
+ this.metrics = this.createInitialMetrics();
1106
+ }
1107
+
1108
+ // ===========================================================================
1109
+ // Lifecycle Methods
1110
+ // ===========================================================================
1111
+
1112
+ /**
1113
+ * Initialize the plugin and establish database connection.
1114
+ */
1115
+ protected async onInitialize(): Promise<void> {
1116
+ this.logger.info('Initializing RuVector PostgreSQL Bridge...');
1117
+ this.initTime = new Date();
1118
+
1119
+ // Create connection manager
1120
+ this.connectionManager = new ConnectionManager(this.ruVectorConfig);
1121
+
1122
+ // Forward connection events
1123
+ this.forwardConnectionEvents();
1124
+
1125
+ // Initialize connection pool
1126
+ try {
1127
+ const connectionResult = await this.connectionManager.initialize();
1128
+ this.logger.info(`Connected to PostgreSQL: ${connectionResult.serverVersion}`);
1129
+ this.logger.info(`RuVector extension version: ${connectionResult.ruVectorVersion}`);
1130
+
1131
+ // Initialize vector operations
1132
+ this.vectorOps = new VectorOps(this.connectionManager, this.ruVectorConfig);
1133
+
1134
+ // Ensure pgvector extension is available
1135
+ await this.ensureExtension();
1136
+
1137
+ this.eventBus.emit('ruvector:initialized', {
1138
+ connectionResult,
1139
+ timestamp: new Date(),
1140
+ });
1141
+ } catch (error) {
1142
+ this.logger.error('Failed to initialize RuVector Bridge', error);
1143
+ throw error;
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * Shutdown the plugin and close database connections.
1149
+ */
1150
+ protected async onShutdown(): Promise<void> {
1151
+ this.logger.info('Shutting down RuVector PostgreSQL Bridge...');
1152
+
1153
+ if (this.connectionManager) {
1154
+ await this.connectionManager.shutdown();
1155
+ this.connectionManager = null;
1156
+ }
1157
+
1158
+ this.vectorOps = null;
1159
+
1160
+ this.eventBus.emit('ruvector:shutdown', {
1161
+ uptime: this.getUptime(),
1162
+ metrics: this.metrics,
1163
+ timestamp: new Date(),
1164
+ });
1165
+ }
1166
+
1167
+ /**
1168
+ * Perform health check.
1169
+ */
1170
+ protected async onHealthCheck(): Promise<Record<string, { healthy: boolean; message?: string; latencyMs?: number }>> {
1171
+ const checks: Record<string, { healthy: boolean; message?: string; latencyMs?: number }> = {};
1172
+
1173
+ // Check connection pool
1174
+ if (this.connectionManager?.isHealthy()) {
1175
+ const poolStats = this.connectionManager.getPoolStats();
1176
+ checks['connection_pool'] = {
1177
+ healthy: true,
1178
+ message: `Pool size: ${poolStats.poolSize}, available: ${poolStats.availableConnections}`,
1179
+ };
1180
+ } else {
1181
+ checks['connection_pool'] = {
1182
+ healthy: false,
1183
+ message: 'Connection pool not initialized or unhealthy',
1184
+ };
1185
+ }
1186
+
1187
+ // Check database connectivity with a simple query
1188
+ if (this.connectionManager) {
1189
+ const startTime = Date.now();
1190
+ try {
1191
+ await this.connectionManager.query('SELECT 1');
1192
+ checks['database'] = {
1193
+ healthy: true,
1194
+ message: 'Database responding',
1195
+ latencyMs: Date.now() - startTime,
1196
+ };
1197
+ } catch (error) {
1198
+ checks['database'] = {
1199
+ healthy: false,
1200
+ message: `Database error: ${(error as Error).message}`,
1201
+ latencyMs: Date.now() - startTime,
1202
+ };
1203
+ }
1204
+ }
1205
+
1206
+ // Check pgvector extension
1207
+ if (this.connectionManager) {
1208
+ try {
1209
+ const result = await this.connectionManager.query<{ extversion: string }>(
1210
+ "SELECT extversion FROM pg_extension WHERE extname = 'vector'"
1211
+ );
1212
+ checks['pgvector'] = {
1213
+ healthy: result.rows.length > 0,
1214
+ message: result.rows.length > 0
1215
+ ? `pgvector version: ${result.rows[0].extversion}`
1216
+ : 'pgvector extension not found',
1217
+ };
1218
+ } catch (error) {
1219
+ checks['pgvector'] = {
1220
+ healthy: false,
1221
+ message: `Error checking pgvector: ${(error as Error).message}`,
1222
+ };
1223
+ }
1224
+ }
1225
+
1226
+ return checks;
1227
+ }
1228
+
1229
+ // ===========================================================================
1230
+ // MCP Tools Registration
1231
+ // ===========================================================================
1232
+
1233
+ /**
1234
+ * Register MCP tools for vector operations.
1235
+ */
1236
+ override registerMCPTools(): MCPToolDefinition[] {
1237
+ return [
1238
+ // Vector Search Tool
1239
+ {
1240
+ name: 'ruvector_search',
1241
+ description: 'Search for similar vectors using HNSW or IVF indexing. Supports cosine, euclidean, and dot product distance metrics.',
1242
+ inputSchema: {
1243
+ type: 'object',
1244
+ properties: {
1245
+ query: {
1246
+ type: 'array',
1247
+ items: { type: 'number' },
1248
+ description: 'Query vector for similarity search',
1249
+ },
1250
+ k: {
1251
+ type: 'number',
1252
+ description: 'Number of nearest neighbors to return',
1253
+ default: 10,
1254
+ },
1255
+ metric: {
1256
+ type: 'string',
1257
+ enum: ['cosine', 'euclidean', 'dot'],
1258
+ description: 'Distance metric to use',
1259
+ default: 'cosine',
1260
+ },
1261
+ tableName: {
1262
+ type: 'string',
1263
+ description: 'Table to search in',
1264
+ default: 'vectors',
1265
+ },
1266
+ filter: {
1267
+ type: 'object',
1268
+ description: 'Metadata filters',
1269
+ },
1270
+ threshold: {
1271
+ type: 'number',
1272
+ description: 'Minimum similarity threshold',
1273
+ },
1274
+ includeMetadata: {
1275
+ type: 'boolean',
1276
+ description: 'Include metadata in results',
1277
+ default: true,
1278
+ },
1279
+ },
1280
+ required: ['query', 'k'],
1281
+ },
1282
+ handler: async (input): Promise<MCPToolResult> => {
1283
+ try {
1284
+ const results = await this.vectorSearch(input as unknown as VectorSearchOptions);
1285
+ this.metrics.searchesPerformed++;
1286
+ return {
1287
+ content: [{
1288
+ type: 'text',
1289
+ text: JSON.stringify({ success: true, results, count: results.length }, null, 2),
1290
+ }],
1291
+ };
1292
+ } catch (error) {
1293
+ return this.createErrorResult(error as Error);
1294
+ }
1295
+ },
1296
+ },
1297
+
1298
+ // Vector Insert Tool
1299
+ {
1300
+ name: 'ruvector_insert',
1301
+ description: 'Insert vectors into a table. Supports batch insertion and upsert.',
1302
+ inputSchema: {
1303
+ type: 'object',
1304
+ properties: {
1305
+ tableName: {
1306
+ type: 'string',
1307
+ description: 'Target table name',
1308
+ },
1309
+ vectors: {
1310
+ type: 'array',
1311
+ items: {
1312
+ type: 'object',
1313
+ properties: {
1314
+ id: { type: 'string' },
1315
+ vector: { type: 'array', items: { type: 'number' } },
1316
+ metadata: { type: 'object' },
1317
+ },
1318
+ required: ['vector'],
1319
+ },
1320
+ description: 'Vectors to insert',
1321
+ },
1322
+ upsert: {
1323
+ type: 'boolean',
1324
+ description: 'Update on conflict',
1325
+ default: false,
1326
+ },
1327
+ },
1328
+ required: ['tableName', 'vectors'],
1329
+ },
1330
+ handler: async (input): Promise<MCPToolResult> => {
1331
+ try {
1332
+ const result = await this.vectorInsert(input as unknown as VectorInsertOptions);
1333
+ this.metrics.vectorsInserted += result.successful;
1334
+ return {
1335
+ content: [{
1336
+ type: 'text',
1337
+ text: JSON.stringify({ success: true, ...result }, null, 2),
1338
+ }],
1339
+ };
1340
+ } catch (error) {
1341
+ return this.createErrorResult(error as Error);
1342
+ }
1343
+ },
1344
+ },
1345
+
1346
+ // Vector Update Tool
1347
+ {
1348
+ name: 'ruvector_update',
1349
+ description: 'Update an existing vector and/or its metadata.',
1350
+ inputSchema: {
1351
+ type: 'object',
1352
+ properties: {
1353
+ tableName: {
1354
+ type: 'string',
1355
+ description: 'Table name',
1356
+ },
1357
+ id: {
1358
+ oneOf: [{ type: 'string' }, { type: 'number' }],
1359
+ description: 'Vector ID to update',
1360
+ },
1361
+ vector: {
1362
+ type: 'array',
1363
+ items: { type: 'number' },
1364
+ description: 'New vector value',
1365
+ },
1366
+ metadata: {
1367
+ type: 'object',
1368
+ description: 'New or updated metadata',
1369
+ },
1370
+ mergeMetadata: {
1371
+ type: 'boolean',
1372
+ description: 'Merge with existing metadata',
1373
+ default: false,
1374
+ },
1375
+ },
1376
+ required: ['tableName', 'id'],
1377
+ },
1378
+ handler: async (input): Promise<MCPToolResult> => {
1379
+ try {
1380
+ const updated = await this.vectorUpdate(input as unknown as VectorUpdateOptions);
1381
+ if (updated) this.metrics.vectorsUpdated++;
1382
+ return {
1383
+ content: [{
1384
+ type: 'text',
1385
+ text: JSON.stringify({ success: true, updated }, null, 2),
1386
+ }],
1387
+ };
1388
+ } catch (error) {
1389
+ return this.createErrorResult(error as Error);
1390
+ }
1391
+ },
1392
+ },
1393
+
1394
+ // Vector Delete Tool
1395
+ {
1396
+ name: 'ruvector_delete',
1397
+ description: 'Delete vectors by ID.',
1398
+ inputSchema: {
1399
+ type: 'object',
1400
+ properties: {
1401
+ tableName: {
1402
+ type: 'string',
1403
+ description: 'Table name',
1404
+ },
1405
+ id: {
1406
+ oneOf: [{ type: 'string' }, { type: 'number' }],
1407
+ description: 'Vector ID to delete',
1408
+ },
1409
+ ids: {
1410
+ type: 'array',
1411
+ items: { oneOf: [{ type: 'string' }, { type: 'number' }] },
1412
+ description: 'Multiple vector IDs to delete',
1413
+ },
1414
+ },
1415
+ required: ['tableName'],
1416
+ },
1417
+ handler: async (input): Promise<MCPToolResult> => {
1418
+ try {
1419
+ const { tableName, id, ids } = input as { tableName: string; id?: string | number; ids?: Array<string | number> };
1420
+
1421
+ if (ids && ids.length > 0) {
1422
+ const result = await this.vectorBulkDelete(tableName, ids);
1423
+ this.metrics.vectorsDeleted += result.successful;
1424
+ return {
1425
+ content: [{
1426
+ type: 'text',
1427
+ text: JSON.stringify({ success: true, ...result }, null, 2),
1428
+ }],
1429
+ };
1430
+ } else if (id !== undefined) {
1431
+ const deleted = await this.vectorDelete(tableName, id);
1432
+ if (deleted) this.metrics.vectorsDeleted++;
1433
+ return {
1434
+ content: [{
1435
+ type: 'text',
1436
+ text: JSON.stringify({ success: true, deleted }, null, 2),
1437
+ }],
1438
+ };
1439
+ } else {
1440
+ return this.createErrorResult(new Error('Either id or ids must be provided'));
1441
+ }
1442
+ } catch (error) {
1443
+ return this.createErrorResult(error as Error);
1444
+ }
1445
+ },
1446
+ },
1447
+
1448
+ // Index Create Tool
1449
+ {
1450
+ name: 'ruvector_create_index',
1451
+ description: 'Create a vector index (HNSW or IVF) for faster similarity search.',
1452
+ inputSchema: {
1453
+ type: 'object',
1454
+ properties: {
1455
+ tableName: {
1456
+ type: 'string',
1457
+ description: 'Table name',
1458
+ },
1459
+ columnName: {
1460
+ type: 'string',
1461
+ description: 'Vector column name',
1462
+ default: 'embedding',
1463
+ },
1464
+ indexType: {
1465
+ type: 'string',
1466
+ enum: ['hnsw', 'ivfflat'],
1467
+ description: 'Index type',
1468
+ default: 'hnsw',
1469
+ },
1470
+ metric: {
1471
+ type: 'string',
1472
+ enum: ['cosine', 'euclidean', 'dot'],
1473
+ description: 'Distance metric',
1474
+ default: 'cosine',
1475
+ },
1476
+ m: {
1477
+ type: 'number',
1478
+ description: 'HNSW M parameter (max connections per layer)',
1479
+ default: 16,
1480
+ },
1481
+ efConstruction: {
1482
+ type: 'number',
1483
+ description: 'HNSW ef_construction parameter',
1484
+ default: 200,
1485
+ },
1486
+ lists: {
1487
+ type: 'number',
1488
+ description: 'IVF lists parameter',
1489
+ },
1490
+ concurrent: {
1491
+ type: 'boolean',
1492
+ description: 'Create index concurrently (non-blocking)',
1493
+ default: true,
1494
+ },
1495
+ },
1496
+ required: ['tableName', 'columnName', 'indexType'],
1497
+ },
1498
+ handler: async (input): Promise<MCPToolResult> => {
1499
+ try {
1500
+ await this.createIndex(input as unknown as VectorIndexOptions);
1501
+ return {
1502
+ content: [{
1503
+ type: 'text',
1504
+ text: JSON.stringify({ success: true, message: 'Index created successfully' }, null, 2),
1505
+ }],
1506
+ };
1507
+ } catch (error) {
1508
+ return this.createErrorResult(error as Error);
1509
+ }
1510
+ },
1511
+ },
1512
+
1513
+ // Index Stats Tool
1514
+ {
1515
+ name: 'ruvector_index_stats',
1516
+ description: 'Get statistics for vector indices.',
1517
+ inputSchema: {
1518
+ type: 'object',
1519
+ properties: {
1520
+ indexName: {
1521
+ type: 'string',
1522
+ description: 'Specific index name (optional)',
1523
+ },
1524
+ tableName: {
1525
+ type: 'string',
1526
+ description: 'Filter by table name (optional)',
1527
+ },
1528
+ },
1529
+ },
1530
+ handler: async (input): Promise<MCPToolResult> => {
1531
+ try {
1532
+ const { indexName, tableName } = input as { indexName?: string; tableName?: string };
1533
+
1534
+ if (indexName) {
1535
+ const stats = await this.getIndexStats(indexName);
1536
+ return {
1537
+ content: [{
1538
+ type: 'text',
1539
+ text: JSON.stringify({ success: true, stats }, null, 2),
1540
+ }],
1541
+ };
1542
+ } else {
1543
+ const indices = await this.listIndices(tableName);
1544
+ return {
1545
+ content: [{
1546
+ type: 'text',
1547
+ text: JSON.stringify({ success: true, indices, count: indices.length }, null, 2),
1548
+ }],
1549
+ };
1550
+ }
1551
+ } catch (error) {
1552
+ return this.createErrorResult(error as Error);
1553
+ }
1554
+ },
1555
+ },
1556
+
1557
+ // Health Check Tool
1558
+ {
1559
+ name: 'ruvector_health',
1560
+ description: 'Check the health status of the RuVector connection and database.',
1561
+ inputSchema: {
1562
+ type: 'object',
1563
+ properties: {},
1564
+ },
1565
+ handler: async (): Promise<MCPToolResult> => {
1566
+ try {
1567
+ const health = await this.healthCheck();
1568
+ return {
1569
+ content: [{
1570
+ type: 'text',
1571
+ text: JSON.stringify({ success: true, health }, null, 2),
1572
+ }],
1573
+ };
1574
+ } catch (error) {
1575
+ return this.createErrorResult(error as Error);
1576
+ }
1577
+ },
1578
+ },
1579
+
1580
+ // Metrics Tool
1581
+ {
1582
+ name: 'ruvector_metrics',
1583
+ description: 'Get performance metrics and statistics.',
1584
+ inputSchema: {
1585
+ type: 'object',
1586
+ properties: {},
1587
+ },
1588
+ handler: async (): Promise<MCPToolResult> => {
1589
+ try {
1590
+ const stats = await this.getStats();
1591
+ return {
1592
+ content: [{
1593
+ type: 'text',
1594
+ text: JSON.stringify({ success: true, metrics: this.metrics, stats }, null, 2),
1595
+ }],
1596
+ };
1597
+ } catch (error) {
1598
+ return this.createErrorResult(error as Error);
1599
+ }
1600
+ },
1601
+ },
1602
+ ];
1603
+ }
1604
+
1605
+ // ===========================================================================
1606
+ // Public Vector Operation Methods
1607
+ // ===========================================================================
1608
+
1609
+ /**
1610
+ * Perform vector similarity search.
1611
+ */
1612
+ async vectorSearch(options: VectorSearchOptions): Promise<VectorSearchResult[]> {
1613
+ this.ensureInitialized();
1614
+ const startTime = Date.now();
1615
+
1616
+ try {
1617
+ const results = await this.vectorOps!.search(options);
1618
+ this.updateQueryMetrics(true, Date.now() - startTime);
1619
+
1620
+ this.emit('search:complete', {
1621
+ searchId: `search-${Date.now()}`,
1622
+ durationMs: Date.now() - startTime,
1623
+ resultCount: results.length,
1624
+ scannedCount: results.length,
1625
+ cacheHit: false,
1626
+ });
1627
+
1628
+ return results;
1629
+ } catch (error) {
1630
+ this.updateQueryMetrics(false, Date.now() - startTime);
1631
+ throw error;
1632
+ }
1633
+ }
1634
+
1635
+ /**
1636
+ * Perform batch vector search.
1637
+ */
1638
+ async vectorBatchSearch(options: BatchVectorOptions): Promise<BulkSearchResult> {
1639
+ this.ensureInitialized();
1640
+ return this.vectorOps!.batchSearch(options);
1641
+ }
1642
+
1643
+ /**
1644
+ * Insert vectors.
1645
+ */
1646
+ async vectorInsert(options: VectorInsertOptions): Promise<BatchResult<string>> {
1647
+ this.ensureInitialized();
1648
+ const result = await this.vectorOps!.insert(options);
1649
+
1650
+ this.emit('vector:batch_complete', {
1651
+ tableName: options.tableName,
1652
+ count: result.total,
1653
+ durationMs: result.durationMs,
1654
+ successCount: result.successful,
1655
+ failedCount: result.failed,
1656
+ });
1657
+
1658
+ return result;
1659
+ }
1660
+
1661
+ /**
1662
+ * Update a vector.
1663
+ */
1664
+ async vectorUpdate(options: VectorUpdateOptions): Promise<boolean> {
1665
+ this.ensureInitialized();
1666
+ const updated = await this.vectorOps!.update(options);
1667
+
1668
+ if (updated) {
1669
+ this.emit('vector:updated', {
1670
+ tableName: options.tableName,
1671
+ vectorId: options.id,
1672
+ dimensions: options.vector?.length ?? 0,
1673
+ });
1674
+ }
1675
+
1676
+ return updated;
1677
+ }
1678
+
1679
+ /**
1680
+ * Delete a vector.
1681
+ */
1682
+ async vectorDelete(tableName: string, id: string | number): Promise<boolean> {
1683
+ this.ensureInitialized();
1684
+ const deleted = await this.vectorOps!.delete(tableName, id);
1685
+
1686
+ if (deleted) {
1687
+ this.emit('vector:deleted', {
1688
+ tableName,
1689
+ vectorId: id,
1690
+ dimensions: 0,
1691
+ });
1692
+ }
1693
+
1694
+ return deleted;
1695
+ }
1696
+
1697
+ /**
1698
+ * Bulk delete vectors.
1699
+ */
1700
+ async vectorBulkDelete(tableName: string, ids: Array<string | number>): Promise<BatchResult> {
1701
+ this.ensureInitialized();
1702
+ return this.vectorOps!.bulkDelete(tableName, ids);
1703
+ }
1704
+
1705
+ /**
1706
+ * Create a vector index.
1707
+ */
1708
+ async createIndex(options: VectorIndexOptions): Promise<void> {
1709
+ this.ensureInitialized();
1710
+ await this.vectorOps!.createIndex(options);
1711
+
1712
+ this.emit('index:created', {
1713
+ indexName: options.indexName ?? `idx_${options.tableName}_${options.columnName}`,
1714
+ tableName: options.tableName,
1715
+ columnName: options.columnName,
1716
+ indexType: options.indexType,
1717
+ });
1718
+ }
1719
+
1720
+ /**
1721
+ * Drop an index.
1722
+ */
1723
+ async dropIndex(indexName: string): Promise<void> {
1724
+ this.ensureInitialized();
1725
+ await this.vectorOps!.dropIndex(indexName);
1726
+
1727
+ this.emit('index:dropped', {
1728
+ indexName,
1729
+ tableName: '',
1730
+ columnName: '',
1731
+ indexType: 'hnsw' as VectorIndexType,
1732
+ });
1733
+ }
1734
+
1735
+ /**
1736
+ * Rebuild an index.
1737
+ */
1738
+ async rebuildIndex(indexName: string): Promise<void> {
1739
+ this.ensureInitialized();
1740
+ await this.vectorOps!.rebuildIndex(indexName);
1741
+
1742
+ this.emit('index:rebuilt', {
1743
+ indexName,
1744
+ tableName: '',
1745
+ columnName: '',
1746
+ indexType: 'hnsw' as VectorIndexType,
1747
+ });
1748
+ }
1749
+
1750
+ /**
1751
+ * Get index statistics.
1752
+ */
1753
+ async getIndexStats(indexName: string): Promise<IndexStats> {
1754
+ this.ensureInitialized();
1755
+ return this.vectorOps!.getIndexStats(indexName);
1756
+ }
1757
+
1758
+ /**
1759
+ * List all indices.
1760
+ */
1761
+ async listIndices(tableName?: string): Promise<IndexStats[]> {
1762
+ this.ensureInitialized();
1763
+ return this.vectorOps!.listIndices(tableName);
1764
+ }
1765
+
1766
+ /**
1767
+ * Get RuVector statistics.
1768
+ */
1769
+ async getStats(): Promise<RuVectorStats> {
1770
+ this.ensureInitialized();
1771
+
1772
+ const poolStats = this.connectionManager!.getPoolStats();
1773
+
1774
+ // Query for vector statistics
1775
+ const result = await this.connectionManager!.query<{
1776
+ table_count: number;
1777
+ total_vectors: number;
1778
+ total_size: number;
1779
+ index_count: number;
1780
+ }>(`
1781
+ SELECT
1782
+ COUNT(DISTINCT c.relname) as table_count,
1783
+ COALESCE(SUM(c.reltuples), 0)::bigint as total_vectors,
1784
+ COALESCE(SUM(pg_total_relation_size(c.oid)), 0)::bigint as total_size,
1785
+ COUNT(DISTINCT i.indexrelid) as index_count
1786
+ FROM pg_class c
1787
+ JOIN pg_attribute a ON a.attrelid = c.oid
1788
+ LEFT JOIN pg_index i ON i.indrelid = c.oid
1789
+ WHERE a.atttypid = 'vector'::regtype
1790
+ AND c.relkind = 'r'
1791
+ `);
1792
+
1793
+ const stats = result.rows[0] ?? {
1794
+ table_count: 0,
1795
+ total_vectors: 0,
1796
+ total_size: 0,
1797
+ index_count: 0,
1798
+ };
1799
+
1800
+ return {
1801
+ version: PLUGIN_VERSION,
1802
+ totalVectors: Number(stats.total_vectors),
1803
+ totalSizeBytes: Number(stats.total_size),
1804
+ numIndices: Number(stats.index_count),
1805
+ numTables: Number(stats.table_count),
1806
+ queryStats: {
1807
+ totalQueries: this.metrics.queriesTotal,
1808
+ avgQueryTimeMs: this.metrics.avgQueryTimeMs,
1809
+ p95QueryTimeMs: this.metrics.avgQueryTimeMs * 1.5, // Approximation
1810
+ p99QueryTimeMs: this.metrics.avgQueryTimeMs * 2, // Approximation
1811
+ cacheHitRate: this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) || 0,
1812
+ },
1813
+ memoryStats: {
1814
+ usedBytes: 0, // Would need OS-level access
1815
+ peakBytes: 0,
1816
+ indexBytes: Number(stats.total_size),
1817
+ cacheBytes: 0,
1818
+ },
1819
+ };
1820
+ }
1821
+
1822
+ // ===========================================================================
1823
+ // Private Helper Methods
1824
+ // ===========================================================================
1825
+
1826
+ /**
1827
+ * Ensure the plugin is initialized.
1828
+ */
1829
+ private ensureInitialized(): void {
1830
+ if (!this.vectorOps || !this.connectionManager) {
1831
+ throw new Error('RuVector Bridge not initialized. Call initialize() first.');
1832
+ }
1833
+ }
1834
+
1835
+ /**
1836
+ * Ensure pgvector extension is installed.
1837
+ */
1838
+ private async ensureExtension(): Promise<void> {
1839
+ try {
1840
+ await this.connectionManager!.query("CREATE EXTENSION IF NOT EXISTS vector");
1841
+ this.logger.debug('pgvector extension ensured');
1842
+ } catch (error) {
1843
+ this.logger.warn('Could not create pgvector extension (may require superuser privileges)', error);
1844
+ }
1845
+ }
1846
+
1847
+ /**
1848
+ * Forward connection manager events to plugin event bus.
1849
+ */
1850
+ private forwardConnectionEvents(): void {
1851
+ const events: RuVectorEventType[] = [
1852
+ 'connection:open',
1853
+ 'connection:close',
1854
+ 'connection:error',
1855
+ 'connection:pool_acquired',
1856
+ 'connection:pool_released',
1857
+ 'query:start',
1858
+ 'query:complete',
1859
+ 'query:error',
1860
+ 'query:slow',
1861
+ ];
1862
+
1863
+ for (const event of events) {
1864
+ this.connectionManager!.on(event, (data) => {
1865
+ this.eventBus.emit(`ruvector:${event}`, data);
1866
+ this.emit(event, data);
1867
+ this.updateMetricsFromEvent(event, data);
1868
+ });
1869
+ }
1870
+ }
1871
+
1872
+ /**
1873
+ * Update metrics from events.
1874
+ */
1875
+ private updateMetricsFromEvent(event: string, _data: unknown): void {
1876
+ switch (event) {
1877
+ case 'connection:pool_acquired':
1878
+ this.metrics.connectionAcquires++;
1879
+ break;
1880
+ case 'connection:pool_released':
1881
+ this.metrics.connectionReleases++;
1882
+ break;
1883
+ case 'connection:error':
1884
+ this.metrics.connectionErrors++;
1885
+ break;
1886
+ case 'query:slow':
1887
+ this.metrics.slowQueries++;
1888
+ break;
1889
+ }
1890
+ }
1891
+
1892
+ /**
1893
+ * Update query metrics.
1894
+ */
1895
+ private updateQueryMetrics(success: boolean, durationMs: number): void {
1896
+ this.metrics.queriesTotal++;
1897
+ if (success) {
1898
+ this.metrics.queriesSucceeded++;
1899
+ } else {
1900
+ this.metrics.queriesFailed++;
1901
+ }
1902
+
1903
+ // Update running average
1904
+ const prevAvg = this.metrics.avgQueryTimeMs;
1905
+ const n = this.metrics.queriesTotal;
1906
+ this.metrics.avgQueryTimeMs = prevAvg + (durationMs - prevAvg) / n;
1907
+ this.metrics.lastQueryTime = durationMs;
1908
+
1909
+ if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
1910
+ this.metrics.slowQueries++;
1911
+ }
1912
+ }
1913
+
1914
+ /**
1915
+ * Create initial metrics object.
1916
+ */
1917
+ private createInitialMetrics(): RuVectorMetrics {
1918
+ return {
1919
+ queriesTotal: 0,
1920
+ queriesSucceeded: 0,
1921
+ queriesFailed: 0,
1922
+ slowQueries: 0,
1923
+ avgQueryTimeMs: 0,
1924
+ vectorsInserted: 0,
1925
+ vectorsUpdated: 0,
1926
+ vectorsDeleted: 0,
1927
+ searchesPerformed: 0,
1928
+ cacheHits: 0,
1929
+ cacheMisses: 0,
1930
+ connectionAcquires: 0,
1931
+ connectionReleases: 0,
1932
+ connectionErrors: 0,
1933
+ lastQueryTime: 0,
1934
+ uptime: 0,
1935
+ };
1936
+ }
1937
+
1938
+ /**
1939
+ * Create error result for MCP tools.
1940
+ */
1941
+ private createErrorResult(error: Error): MCPToolResult {
1942
+ return {
1943
+ content: [{
1944
+ type: 'text',
1945
+ text: JSON.stringify({
1946
+ success: false,
1947
+ error: error.message,
1948
+ code: (error as { code?: string }).code,
1949
+ }, null, 2),
1950
+ }],
1951
+ isError: true,
1952
+ };
1953
+ }
1954
+
1955
+ /**
1956
+ * Get plugin uptime.
1957
+ */
1958
+ override getUptime(): number {
1959
+ if (!this.initTime) return 0;
1960
+ return Date.now() - this.initTime.getTime();
1961
+ }
1962
+
1963
+ /**
1964
+ * Get current metrics.
1965
+ */
1966
+ getMetrics(): RuVectorMetrics {
1967
+ return {
1968
+ ...this.metrics,
1969
+ uptime: this.getUptime(),
1970
+ };
1971
+ }
1972
+ }
1973
+
1974
+ // ============================================================================
1975
+ // Factory Function
1976
+ // ============================================================================
1977
+
1978
+ /**
1979
+ * Create a new RuVector Bridge plugin instance.
1980
+ *
1981
+ * @example
1982
+ * ```typescript
1983
+ * const bridge = createRuVectorBridge({
1984
+ * host: 'localhost',
1985
+ * port: 5432,
1986
+ * database: 'vectors',
1987
+ * user: 'postgres',
1988
+ * password: 'password',
1989
+ * });
1990
+ * ```
1991
+ */
1992
+ export function createRuVectorBridge(config: RuVectorConfig): RuVectorBridge {
1993
+ return new RuVectorBridge(config);
1994
+ }
1995
+
1996
+ // ============================================================================
1997
+ // Default Export
1998
+ // ============================================================================
1999
+
2000
+ export default RuVectorBridge;