UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

710 lines (709 loc) 21.2 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { promises as fs } from "fs"; import * as path from "path"; import { pathToFileURL } from "url"; import { logger } from "../monitoring/logger.js"; const DEFAULT_LOAD_TIMEOUT = 3e4; const ALLOWED_PERMISSION_PATTERNS = [ /^network:[a-z0-9.-]+$/, /^storage:(local|session)$/, /^frames:(read|write)$/, /^events:(emit|listen)$/ ]; const BLOCKED_DOMAINS = [ "localhost", "127.0.0.1", "0.0.0.0", "*.local", "internal.*" ]; class ExtensionLoader { loadedExtensions = /* @__PURE__ */ new Map(); extensionInstances = /* @__PURE__ */ new Map(); eventHandlers = /* @__PURE__ */ new Map(); grantedPermissions = /* @__PURE__ */ new Map(); /** * Load an extension from various sources */ async loadExtension(options) { const { source, timeout = DEFAULT_LOAD_TIMEOUT } = options; try { const { type, uri } = this.parseSource(source); logger.info("Loading extension", { source, type }); const loadPromise = this.loadFromSource(type, uri, options); const result = await this.withTimeout(loadPromise, timeout); if (!result.success || !result.extension) { return result; } const validation = this.validateExtension(result.extension); if (!validation.valid) { return { success: false, error: `Extension validation failed: ${validation.errors.join(", ")}`, warnings: validation.warnings }; } if (!options.skipPermissionCheck) { const permissionCheck = await this.verifyPermissions( result.extension, options.permissions ); if (!permissionCheck.success) { return permissionCheck; } } const initResult = await this.initializeExtension( result.extension, options ); if (!initResult.success) { return initResult; } const extensionId = this.generateExtensionId(result.extension); this.registerExtension( extensionId, result.extension, source, type, options ); logger.info("Extension loaded successfully", { extensionId, name: result.extension.name, version: result.extension.version }); return { success: true, extension: result.extension, extensionId, warnings: validation.warnings }; } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("Failed to load extension", { source, error: message }); return { success: false, error: `Failed to load extension: ${message}` }; } } /** * Unload an extension */ async unloadExtension(extensionId) { const extension = this.extensionInstances.get(extensionId); const state = this.loadedExtensions.get(extensionId); if (!extension || !state) { logger.warn("Extension not found for unload", { extensionId }); return false; } try { await extension.destroy(); this.extensionInstances.delete(extensionId); this.loadedExtensions.delete(extensionId); this.grantedPermissions.delete(extensionId); logger.info("Extension unloaded", { extensionId }); return true; } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("Failed to unload extension", { extensionId, error: message }); return false; } } /** * Get all loaded extensions */ getLoadedExtensions() { return Array.from(this.loadedExtensions.values()); } /** * Get extension by ID */ getExtension(extensionId) { return this.extensionInstances.get(extensionId); } /** * Get extension state by ID */ getExtensionState(extensionId) { return this.loadedExtensions.get(extensionId); } /** * Parse source string to determine type and URI */ parseSource(source) { if (source.startsWith("https://") || source.startsWith("http://")) { return { type: "url", uri: source }; } if (source.startsWith("file://")) { return { type: "file", uri: source.slice(7) }; } if (source.startsWith("npm:")) { return { type: "npm", uri: source.slice(4) }; } if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) { return { type: "file", uri: source }; } return { type: "npm", uri: source }; } /** * Load extension from source based on type */ async loadFromSource(type, uri, options) { switch (type) { case "url": return this.loadFromUrl(uri); case "file": return this.loadFromFile(uri); case "npm": return this.loadFromNpm(uri); default: return { success: false, error: `Unknown source type: ${type}` }; } } /** * Load extension from URL */ async loadFromUrl(url) { try { const parsedUrl = new URL(url); if (this.isBlockedDomain(parsedUrl.hostname)) { return { success: false, error: `Blocked domain: ${parsedUrl.hostname}` }; } if (parsedUrl.protocol !== "https:" && process.env["NODE_ENV"] === "production") { return { success: false, error: "Only HTTPS URLs are allowed in production" }; } const response = await fetch(url); if (!response.ok) { return { success: false, error: `HTTP ${response.status}: ${response.statusText}` }; } const code = await response.text(); const manifestUrl = url.replace(/\.js$/, ".manifest.json"); let manifest; try { const manifestResponse = await fetch(manifestUrl); if (manifestResponse.ok) { manifest = await manifestResponse.json(); } } catch { } const extension = await this.evaluateExtensionCode(code, url); if (manifest) { extension.manifest = manifest; } return { success: true, extension }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { success: false, error: `Failed to load from URL: ${message}` }; } } /** * Load extension from local file */ async loadFromFile(filePath) { try { const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath); try { await fs.access(absolutePath); } catch { return { success: false, error: `File not found: ${absolutePath}` }; } const manifestPath = absolutePath.replace(/\.js$/, ".manifest.json"); let manifest; try { const manifestContent = await fs.readFile(manifestPath, "utf-8"); manifest = JSON.parse(manifestContent); } catch { try { const packagePath = path.join( path.dirname(absolutePath), "package.json" ); const packageContent = await fs.readFile(packagePath, "utf-8"); const pkg = JSON.parse(packageContent); if (pkg.stackmemory) { manifest = pkg.stackmemory; } } catch { } } const fileUrl = pathToFileURL(absolutePath).href; const module = await import(fileUrl); const extension = module.default || module; if (!this.isExtensionLike(extension)) { return { success: false, error: "Module does not export a valid extension" }; } if (manifest) { extension.manifest = manifest; } return { success: true, extension }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { success: false, error: `Failed to load from file: ${message}` }; } } /** * Load extension from NPM package */ async loadFromNpm(packageName) { try { const { name, version } = this.parseNpmPackage(packageName); let module; try { module = await import(name); } catch { try { const nodeModulesPath = path.join( process.cwd(), "node_modules", name ); const packageJsonPath = path.join(nodeModulesPath, "package.json"); const packageJson = JSON.parse( await fs.readFile(packageJsonPath, "utf-8") ); const entryPoint = packageJson.main || "index.js"; const entryPath = path.join(nodeModulesPath, entryPoint); module = await import(pathToFileURL(entryPath).href); } catch (innerError) { return { success: false, error: `Package not found: ${name}. Try running: npm install ${name}` }; } } const extension = module.default || module; if (!this.isExtensionLike(extension)) { return { success: false, error: "Package does not export a valid extension" }; } return { success: true, extension }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { success: false, error: `Failed to load from NPM: ${message}` }; } } /** * Evaluate extension code (for URL sources) */ async evaluateExtensionCode(code, sourceUrl) { const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`; try { const module = await import(dataUrl); const extension = module.default || module; if (!this.isExtensionLike(extension)) { throw new Error("Code does not export a valid extension"); } return extension; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to evaluate extension code: ${message}`); } } /** * Check if object looks like an extension */ isExtensionLike(obj) { if (!obj || typeof obj !== "object") return false; const ext = obj; return typeof ext["name"] === "string" && typeof ext["version"] === "string" && typeof ext["init"] === "function" && typeof ext["destroy"] === "function"; } /** * Validate extension structure and metadata */ validateExtension(extension) { const errors = []; const warnings = []; if (!extension.name || typeof extension.name !== "string") { errors.push("Extension must have a valid name"); } if (!extension.version || typeof extension.version !== "string") { errors.push("Extension must have a valid version"); } if (extension.version && !/^\d+\.\d+\.\d+/.test(extension.version)) { warnings.push("Version should follow semver format (x.y.z)"); } if (typeof extension.init !== "function") { errors.push("Extension must have an init method"); } if (typeof extension.destroy !== "function") { errors.push("Extension must have a destroy method"); } if (extension.tools) { if (!Array.isArray(extension.tools)) { errors.push("Extension tools must be an array"); } else { extension.tools.forEach((tool, index) => { if (!tool.name) { errors.push(`Tool at index ${index} must have a name`); } if (!tool.execute || typeof tool.execute !== "function") { errors.push( `Tool "${tool.name || index}" must have an execute function` ); } }); } } if (extension.manifest) { const manifestValidation = this.validateManifest(extension.manifest); errors.push(...manifestValidation.errors); warnings.push(...manifestValidation.warnings); } return { valid: errors.length === 0, errors, warnings }; } /** * Validate extension manifest */ validateManifest(manifest) { const errors = []; const warnings = []; if (!manifest.name) { errors.push("Manifest must have a name"); } if (!manifest.version) { errors.push("Manifest must have a version"); } if (!manifest.permissions || !Array.isArray(manifest.permissions)) { errors.push("Manifest must have a permissions array"); } else { manifest.permissions.forEach((perm) => { if (!this.isValidPermission(perm)) { errors.push(`Invalid permission: ${perm}`); } }); } return { valid: errors.length === 0, errors, warnings }; } /** * Check if permission string is valid */ isValidPermission(permission) { return ALLOWED_PERMISSION_PATTERNS.some( (pattern) => pattern.test(permission) ); } /** * Verify extension permissions */ async verifyPermissions(extension, overridePermissions) { const requestedPermissions = overridePermissions || extension.manifest?.permissions || []; for (const perm of requestedPermissions) { if (perm.startsWith("network:")) { const domain = perm.slice(8); if (this.isBlockedDomain(domain)) { return { success: false, error: `Permission denied: network access to ${domain} is blocked` }; } } } logger.debug("Permission verification passed", { extension: extension.name, permissions: requestedPermissions }); return { success: true, extension }; } /** * Initialize extension with context */ async initializeExtension(extension, options) { try { const extensionId = this.generateExtensionId(extension); const permissions = options.permissions || extension.manifest?.permissions || []; const context = this.createExtensionContext(extensionId, permissions); this.grantedPermissions.set(extensionId, new Set(permissions)); await extension.init(context); return { success: true, extension }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { success: false, error: `Extension initialization failed: ${message}` }; } } /** * Create extension context with permission-gated access */ createExtensionContext(extensionId, permissions) { const permSet = new Set(permissions); const context = { extensionId, permissions, // State management (always available, scoped to extension) state: { get: async (key) => { return void 0; }, set: async (key, value) => { }, delete: async (key) => { } }, // Sandboxed fetch (always available) fetch: this.createSandboxedFetch(extensionId, permSet), // Event system emit: (event, data) => { if (!permSet.has("events:emit")) { logger.warn("Extension lacks events:emit permission", { extensionId, event }); return; } this.emitEvent(event, data); }, on: (event, handler) => { if (!permSet.has("events:listen")) { logger.warn("Extension lacks events:listen permission", { extensionId, event }); return () => { }; } return this.addEventListener(event, handler); }, off: (event, handler) => { this.removeEventListener(event, handler); } }; if (permSet.has("frames:read") || permSet.has("frames:write")) { context.frames = { get: async (frameId) => { return void 0; }, list: async () => { return []; } }; if (permSet.has("frames:write")) { context.frames.create = async (options) => { throw new Error("Frame creation not implemented"); }; context.frames.update = async (frameId, data) => { throw new Error("Frame update not implemented"); }; } } return context; } /** * Create sandboxed fetch that respects network permissions */ createSandboxedFetch(extensionId, permissions) { return async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; const parsedUrl = new URL(url); const domain = parsedUrl.hostname; const hasPermission = Array.from(permissions).some((perm) => { if (!perm.startsWith("network:")) return false; const allowedDomain = perm.slice(8); if (allowedDomain.startsWith("*.")) { const baseDomain = allowedDomain.slice(2); return domain.endsWith(baseDomain); } return domain === allowedDomain; }); if (!hasPermission) { throw new Error(`Network access denied for domain: ${domain}`); } if (this.isBlockedDomain(domain)) { throw new Error(`Network access blocked for domain: ${domain}`); } return fetch(input, init); }; } /** * Check if domain is blocked */ isBlockedDomain(domain) { return BLOCKED_DOMAINS.some((blocked) => { if (blocked.startsWith("*.")) { return domain.endsWith(blocked.slice(1)); } if (blocked.endsWith(".*")) { return domain.startsWith(blocked.slice(0, -1)); } return domain === blocked; }); } /** * Register loaded extension */ registerExtension(extensionId, extension, source, sourceType, options) { const state = { id: extensionId, name: extension.name, version: extension.version, source, sourceType, permissions: options.permissions || extension.manifest?.permissions || [], loadedAt: Date.now(), status: "active" }; this.loadedExtensions.set(extensionId, state); this.extensionInstances.set(extensionId, extension); } /** * Generate unique extension ID */ generateExtensionId(extension) { return `${extension.name}@${extension.version}`; } /** * Parse NPM package string */ parseNpmPackage(packageName) { const atIndex = packageName.lastIndexOf("@"); if (atIndex > 0) { return { name: packageName.slice(0, atIndex), version: packageName.slice(atIndex + 1) }; } return { name: packageName }; } /** * Wrap promise with timeout */ async withTimeout(promise, timeoutMs) { return Promise.race([ promise, new Promise( (_, reject) => setTimeout( () => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs ) ) ]); } /** * Emit event to all listeners */ emitEvent(event, data) { const handlers = this.eventHandlers.get(event); if (!handlers) return; handlers.forEach((handler) => { try { const result = handler(data); if (result instanceof Promise) { result.catch((error) => { logger.error("Event handler error", { event, error }); }); } } catch (error) { logger.error("Event handler error", { event, error }); } }); } /** * Add event listener */ addEventListener(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, /* @__PURE__ */ new Set()); } this.eventHandlers.get(event).add(handler); return () => this.removeEventListener(event, handler); } /** * Remove event listener */ removeEventListener(event, handler) { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.delete(handler); } } /** * Disable an extension without unloading */ async disableExtension(extensionId) { const state = this.loadedExtensions.get(extensionId); if (!state) return false; state.status = "disabled"; return true; } /** * Enable a disabled extension */ async enableExtension(extensionId) { const state = this.loadedExtensions.get(extensionId); if (!state || state.status !== "disabled") return false; state.status = "active"; return true; } /** * Unload all extensions */ async unloadAll() { const extensionIds = Array.from(this.loadedExtensions.keys()); for (const extensionId of extensionIds) { await this.unloadExtension(extensionId); } } } let loaderInstance; function getExtensionLoader() { if (!loaderInstance) { loaderInstance = new ExtensionLoader(); } return loaderInstance; } async function loadExtension(source, options) { const loader = getExtensionLoader(); return loader.loadExtension({ source, ...options }); } async function unloadExtension(extensionId) { const loader = getExtensionLoader(); return loader.unloadExtension(extensionId); } export { ExtensionLoader, getExtensionLoader, loadExtension, unloadExtension }; //# sourceMappingURL=loader.js.map