open-brainy/src/augmentations/discovery/localDiscovery.ts
David Snelling c303ead318 feat: add browser environment compatibility support
- Remove top-level Node.js imports that break bundlers
- Use universal adapters for crypto operations
- Add dynamic imports for Node.js-specific modules
- Add browser field to package.json for bundler hints
- Maintain full Node.js functionality while enabling browser usage

This allows Brainy to be used with modern bundlers (Vite, Webpack, etc.)
without requiring Node.js polyfills. Browser environments get core features
while Node.js retains all capabilities including filesystem and networking.
2025-09-17 15:48:02 -07:00

315 lines
No EOL
9.5 KiB
TypeScript

/**
* Local Augmentation Discovery
*
* Discovers augmentations installed locally in node_modules
* and built-in augmentations that ship with Brainy
*
* NOTE: This is a Node.js-only feature that requires filesystem access
*/
import { AugmentationManifest } from '../manifest.js'
// Node.js modules - dynamically imported to avoid bundler issues
let fs: any = null
let path: any = null
// Load Node.js modules if available
if (typeof window === 'undefined') {
try {
fs = await import('node:fs')
path = await import('node:path')
} catch (e) {
// Will throw error in methods if not available
}
}
// Create compatibility layer for sync methods
const existsSync = fs?.existsSync || (() => { throw new Error('Filesystem not available') })
const readdirSync = fs?.readdirSync || (() => { throw new Error('Filesystem not available') })
const readFileSync = fs?.readFileSync || (() => { throw new Error('Filesystem not available') })
const join = path?.join || ((...args: string[]) => args.join('/'))
export interface LocalAugmentation {
id: string
name: string
source: 'builtin' | 'npm' | 'local'
path: string
manifest?: AugmentationManifest
package?: {
name: string
version: string
description?: string
}
}
/**
* Discovers augmentations installed locally
*/
export class LocalAugmentationDiscovery {
private builtInAugmentations: Map<string, LocalAugmentation> = new Map()
private installedAugmentations: Map<string, LocalAugmentation> = new Map()
constructor(private options: {
brainyPath?: string
projectPath?: string
scanNodeModules?: boolean
} = {}) {
this.options = {
brainyPath: this.options.brainyPath || this.findBrainyPath(),
projectPath: this.options.projectPath || process.cwd(),
scanNodeModules: this.options.scanNodeModules ?? true
}
// Register built-in augmentations
this.registerBuiltIn()
}
/**
* Register built-in augmentations that ship with Brainy
*/
private registerBuiltIn(): void {
const builtIn = [
{ id: 'cache', name: 'Cache', path: 'cacheAugmentation' },
{ id: 'batch', name: 'Batch Processing', path: 'batchProcessingAugmentation' },
{ id: 'entity-registry', name: 'Entity Registry', path: 'entityRegistryAugmentation' },
{ id: 'index', name: 'Index', path: 'indexAugmentation' },
{ id: 'metrics', name: 'Metrics', path: 'metricsAugmentation' },
{ id: 'monitoring', name: 'Monitoring', path: 'monitoringAugmentation' },
{ id: 'connection-pool', name: 'Connection Pool', path: 'connectionPoolAugmentation' },
{ id: 'request-deduplicator', name: 'Request Deduplicator', path: 'requestDeduplicatorAugmentation' },
{ id: 'api-server', name: 'API Server', path: 'apiServerAugmentation' },
{ id: 'neural-import', name: 'Neural Import', path: 'neuralImport' },
{ id: 'intelligent-verb-scoring', name: 'Intelligent Verb Scoring', path: 'intelligentVerbScoringAugmentation' },
{ id: 'universal-display', name: 'Universal Display', path: 'universalDisplayAugmentation' },
// Storage augmentations
{ id: 'memory-storage', name: 'Memory Storage', path: 'storageAugmentations', export: 'MemoryStorageAugmentation' },
{ id: 'filesystem-storage', name: 'FileSystem Storage', path: 'storageAugmentations', export: 'FileSystemStorageAugmentation' },
{ id: 'opfs-storage', name: 'OPFS Storage', path: 'storageAugmentations', export: 'OPFSStorageAugmentation' },
{ id: 's3-storage', name: 'S3 Storage', path: 'storageAugmentations', export: 'S3StorageAugmentation' },
]
for (const aug of builtIn) {
this.builtInAugmentations.set(aug.id, {
id: aug.id,
name: aug.name,
source: 'builtin',
path: `@soulcraft/brainy/augmentations/${aug.path}`,
package: {
name: '@soulcraft/brainy',
version: 'builtin',
description: `Built-in ${aug.name} augmentation`
}
})
}
}
/**
* Find Brainy installation path
*/
private findBrainyPath(): string {
// Try to find brainy in node_modules
const possiblePaths = [
join(process.cwd(), 'node_modules', '@soulcraft', 'brainy'),
join(process.cwd(), 'node_modules', 'brainy'),
join(process.cwd(), '..', 'node_modules', '@soulcraft', 'brainy'),
]
for (const path of possiblePaths) {
if (existsSync(path)) {
return path
}
}
// Fallback to current directory
return process.cwd()
}
/**
* Discover all augmentations
*/
async discoverAll(): Promise<LocalAugmentation[]> {
const augmentations: LocalAugmentation[] = []
// Add built-in augmentations
augmentations.push(...this.builtInAugmentations.values())
// Scan node_modules if enabled
if (this.options.scanNodeModules) {
const installed = await this.scanNodeModules()
augmentations.push(...installed)
}
// Scan local project
const local = await this.scanLocalProject()
augmentations.push(...local)
return augmentations
}
/**
* Scan node_modules for installed augmentations
*/
private async scanNodeModules(): Promise<LocalAugmentation[]> {
const augmentations: LocalAugmentation[] = []
const nodeModulesPath = join(this.options.projectPath!, 'node_modules')
if (!existsSync(nodeModulesPath)) {
return augmentations
}
// Scan @brainy/* packages
const brainyPath = join(nodeModulesPath, '@brainy')
if (existsSync(brainyPath)) {
const packages = readdirSync(brainyPath)
for (const pkg of packages) {
const augmentation = await this.loadPackageAugmentation(join(brainyPath, pkg))
if (augmentation) {
augmentations.push(augmentation)
}
}
}
// Scan packages with brainy-augmentation keyword
const packages = readdirSync(nodeModulesPath)
for (const pkg of packages) {
if (pkg.startsWith('@') || pkg.startsWith('.')) continue
const pkgPath = join(nodeModulesPath, pkg)
const packageJson = this.loadPackageJson(pkgPath)
if (packageJson?.keywords?.includes('brainy-augmentation')) {
const augmentation = await this.loadPackageAugmentation(pkgPath)
if (augmentation) {
augmentations.push(augmentation)
}
}
}
return augmentations
}
/**
* Scan local project for augmentations
*/
private async scanLocalProject(): Promise<LocalAugmentation[]> {
const augmentations: LocalAugmentation[] = []
// Check for augmentations directory
const augPath = join(this.options.projectPath!, 'augmentations')
if (existsSync(augPath)) {
const files = readdirSync(augPath)
for (const file of files) {
if (file.endsWith('.ts') || file.endsWith('.js')) {
const name = file.replace(/\.(ts|js)$/, '')
augmentations.push({
id: name,
name: this.humanizeName(name),
source: 'local',
path: join(augPath, file)
})
}
}
}
return augmentations
}
/**
* Load augmentation from package
*/
private async loadPackageAugmentation(pkgPath: string): Promise<LocalAugmentation | null> {
const packageJson = this.loadPackageJson(pkgPath)
if (!packageJson) return null
// Check if it's a brainy augmentation
const isBrainyAug = packageJson.keywords?.includes('brainy-augmentation') ||
packageJson.brainy?.type === 'augmentation'
if (!isBrainyAug) return null
const manifest = packageJson.brainy?.manifest || null
return {
id: packageJson.brainy?.id || packageJson.name.replace('@brainy/', ''),
name: packageJson.brainy?.name || packageJson.name,
source: 'npm',
path: pkgPath,
manifest,
package: {
name: packageJson.name,
version: packageJson.version,
description: packageJson.description
}
}
}
/**
* Load package.json
*/
private loadPackageJson(pkgPath: string): any {
const packageJsonPath = join(pkgPath, 'package.json')
if (!existsSync(packageJsonPath)) return null
try {
return JSON.parse(readFileSync(packageJsonPath, 'utf8'))
} catch {
return null
}
}
/**
* Convert name to human-readable format
*/
private humanizeName(name: string): string {
return name
.replace(/[-_]/g, ' ')
.replace(/augmentation/gi, '')
.replace(/\b\w/g, l => l.toUpperCase())
.trim()
}
/**
* Get built-in augmentations
*/
getBuiltIn(): LocalAugmentation[] {
return Array.from(this.builtInAugmentations.values())
}
/**
* Get installed augmentations
*/
getInstalled(): LocalAugmentation[] {
return Array.from(this.installedAugmentations.values())
}
/**
* Check if augmentation is installed
*/
isInstalled(id: string): boolean {
return this.builtInAugmentations.has(id) ||
this.installedAugmentations.has(id)
}
/**
* Get import path for augmentation
*/
getImportPath(id: string): string | null {
const aug = this.builtInAugmentations.get(id) ||
this.installedAugmentations.get(id)
return aug?.path || null
}
/**
* Load augmentation module dynamically
*/
async loadAugmentation(id: string): Promise<any> {
const path = this.getImportPath(id)
if (!path) {
throw new Error(`Augmentation ${id} not found`)
}
// Dynamic import
const module = await import(path)
return module.default || module
}
}