diff --git a/src/brainy.ts b/src/brainy.ts index 45f7be5d..234edf70 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -13,7 +13,8 @@ import { BaseStorage } from './storage/baseStorage.js' import { StorageAdapter, Vector, DistanceFunction, EmbeddingFunction, GraphVerb } from './coreTypes.js' import { defaultEmbeddingFunction, - cosineDistance + cosineDistance, + getBrainyVersion } from './utils/index.js' import { embeddingManager } from './embeddings/EmbeddingManager.js' import { matchesMetadataFilter } from './utils/metadataFilter.js' @@ -33,6 +34,8 @@ import { BlobStorage } from './storage/cow/BlobStorage.js' import { NULL_HASH } from './storage/cow/constants.js' import { createPipeline } from './streaming/pipeline.js' import { configureLogger, LogLevel } from './utils/logger.js' +import { PluginRegistry } from './plugin.js' +import type { BrainyPlugin, BrainyPluginContext } from './plugin.js' import { TransactionManager } from './transaction/TransactionManager.js' import { ValidationConfig, @@ -142,6 +145,9 @@ export class Brainy implements BrainyInterface { error: typeof console.error } + // Plugin system + private pluginRegistry = new PluginRegistry() + // Sub-APIs (lazy-loaded) private _neural?: ImprovedNeuralAPI private _nlp?: NaturalLanguageProcessor @@ -267,6 +273,9 @@ export class Brainy implements BrainyInterface { (this.storage as any).enableCOWLightweight((this.config.storage as any)?.branch || 'main') } + // Auto-detect and activate plugins (e.g., @soulcraft/brainy-cortex) + await this.loadPlugins() + // Setup index now that we have storage this.index = this.setupIndex() @@ -6534,6 +6543,51 @@ export class Brainy implements BrainyInterface { await this.metadataIndex.detectAndRepairCorruption() } + /** + * Register a plugin manually. + * + * Must be called BEFORE init(). Plugins registered after init() + * will not be activated. + */ + use(plugin: BrainyPlugin): this { + this.pluginRegistry.register(plugin) + return this + } + + /** + * Get list of active plugin names. + */ + getActivePlugins(): string[] { + return this.pluginRegistry.getActivePlugins() + } + + /** + * Auto-detect and activate plugins. + * Called internally during init(). + */ + private async loadPlugins(): Promise { + // Auto-detect installed plugins (e.g., @soulcraft/brainy-cortex) + const pluginPackages = (this.config as any).plugins as string[] | undefined + await this.pluginRegistry.autoDetect(pluginPackages || []) + + // Create plugin context + const context: BrainyPluginContext = { + registerProvider: (key, impl) => this.pluginRegistry.registerProvider(key, impl), + version: getBrainyVersion() + } + + // Activate all registered plugins + const activated = await this.pluginRegistry.activateAll(context) + if (activated.length > 0) { + // Only log if not in silent mode + if (!this.config.silent) { + for (const name of activated) { + console.log(`[brainy] Plugin activated: ${name}`) + } + } + } + } + /** * Close and cleanup * @@ -6547,6 +6601,9 @@ export class Brainy implements BrainyInterface { await this.index.flush() } + // Deactivate plugins + await this.pluginRegistry.deactivateAll() + // Shutdown augmentations const augs = this.augmentationRegistry.getAll() for (const aug of augs) { diff --git a/src/index.ts b/src/index.ts index 28d2953d..e6089c15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,6 +109,10 @@ export { // Export version utilities export { getBrainyVersion } from './utils/version.js' +// Export plugin system +export type { BrainyPlugin, BrainyPluginContext, StorageAdapterFactory } from './plugin.js' +export { PluginRegistry } from './plugin.js' + // Export embedding functionality import { UniversalSentenceEncoder, diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 00000000..53df78cf --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,184 @@ +/** + * Brainy Plugin System + * + * Simple plugin architecture for two use cases: + * 1. Native acceleration (@soulcraft/brainy-cortex) + * 2. Custom storage adapters (e.g., Redis, DynamoDB, custom backends) + * + * Plugins are auto-detected by package name or registered manually. + */ + +import type { StorageAdapter } from './coreTypes.js' + +/** + * Plugin interface — all brainy plugins must implement this. + */ +export interface BrainyPlugin { + /** Unique plugin name (typically the npm package name) */ + name: string + + /** + * Called by brainy during init() to activate the plugin. + * Return true if activation succeeded, false to skip. + */ + activate(context: BrainyPluginContext): Promise + + /** + * Called when brainy.close() is invoked. Optional cleanup. + */ + deactivate?(): Promise +} + +/** + * Context passed to plugins during activation. + */ +export interface BrainyPluginContext { + /** + * Register a provider for a named subsystem. + * + * Well-known provider keys (used by cortex): + * - 'metadataIndex' — MetadataIndexManager replacement + * - 'graphIndex' — GraphAdjacencyIndex replacement + * - 'entityIdMapper' — EntityIdMapper replacement + * - 'cache' — UnifiedCache replacement + * - 'hnsw' — HNSWIndex replacement + * - 'roaring' — RoaringBitmap32 replacement + * - 'embeddings' — Embedding engine replacement + * - 'distance' — Distance function overrides + * - 'msgpack' — Msgpack encode/decode + * + * Storage adapter keys: + * - 'storage:' — Custom storage adapter factory + */ + registerProvider(key: string, implementation: unknown): void + + /** Brainy version for compatibility checks */ + readonly version: string +} + +/** + * Storage adapter factory — plugins register these to provide + * new storage backends that users reference by name. + * + * Example: A Redis plugin registers 'storage:redis', then users + * can use `new Brainy({ storage: 'redis', redis: { host: '...' } })` + */ +export interface StorageAdapterFactory { + create(config: Record): StorageAdapter | Promise + name: string +} + +/** + * Plugin registry — manages plugin lifecycle and provider resolution. + */ +export class PluginRegistry { + private plugins: Map = new Map() + private providers: Map = new Map() + private activated: Set = new Set() + + /** + * Register a plugin manually. + */ + register(plugin: BrainyPlugin): void { + this.plugins.set(plugin.name, plugin) + } + + /** + * Auto-detect known plugins by attempting dynamic import. + * Additional package names can be passed for third-party plugins. + */ + async autoDetect(additionalPackages: string[] = []): Promise { + const packages = [ + '@soulcraft/brainy-cortex', + ...additionalPackages + ] + + for (const pkg of packages) { + try { + const mod = await import(pkg) + const plugin: BrainyPlugin = mod.default || mod + if (plugin && typeof plugin.activate === 'function' && plugin.name) { + this.plugins.set(plugin.name, plugin) + } + } catch { + // Package not installed — skip silently + } + } + } + + /** + * Activate all registered plugins. + */ + async activateAll(context: BrainyPluginContext): Promise { + const activated: string[] = [] + + for (const [name, plugin] of this.plugins) { + if (this.activated.has(name)) continue + + try { + const success = await plugin.activate(context) + if (success) { + this.activated.add(name) + activated.push(name) + } + } catch (error) { + console.warn(`[brainy] Plugin ${name} failed to activate:`, error) + } + } + + return activated + } + + /** + * Deactivate all plugins (called during close()). + */ + async deactivateAll(): Promise { + for (const [name, plugin] of this.plugins) { + if (!this.activated.has(name)) continue + try { + await plugin.deactivate?.() + this.activated.delete(name) + } catch { + // Non-fatal + } + } + } + + /** + * Get a registered provider by key. + */ + getProvider(key: string): T | undefined { + return this.providers.get(key) as T | undefined + } + + /** + * Check if a provider is registered. + */ + hasProvider(key: string): boolean { + return this.providers.has(key) + } + + /** + * Register a provider (called by plugins via BrainyPluginContext). + */ + registerProvider(key: string, implementation: unknown): void { + this.providers.set(key, implementation) + } + + /** + * Get a storage adapter factory by name. + */ + getStorageFactory(name: string): StorageAdapterFactory | undefined { + return this.providers.get(`storage:${name}`) as StorageAdapterFactory | undefined + } + + /** Get active plugin names */ + getActivePlugins(): string[] { + return [...this.activated] + } + + /** Check if any plugins are active */ + hasActivePlugins(): boolean { + return this.activated.size > 0 + } +}