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.

381 lines (380 loc) 10.9 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { v4 as uuidv4 } from "uuid"; import { Logger } from "../monitoring/logger.js"; let chromadbModule = null; async function getChromaDB() { if (!chromadbModule) { try { chromadbModule = await import("chromadb"); } catch { throw new Error( "chromadb is not installed. Install it with: npm install chromadb" ); } } return chromadbModule; } class ChromaDBAdapter { client = null; collection = null; logger; config; userId; teamId; constructor(config, userId, teamId) { this.config = config; this.userId = userId; this.teamId = teamId; this.logger = new Logger("ChromaDBAdapter"); } /** * Factory method to create and initialize the adapter */ static async create(config, userId, teamId) { const adapter = new ChromaDBAdapter(config, userId, teamId); await adapter.initClient(); return adapter; } async initClient() { const chromadb = await getChromaDB(); this.client = new chromadb.CloudClient({ apiKey: this.config.apiKey, tenant: this.config.tenant, database: this.config.database }); } async initialize() { try { if (!this.client) { await this.initClient(); } const collectionName = this.config.collectionName || "stackmemory_contexts"; this.collection = await this.client.getOrCreateCollection({ name: collectionName, metadata: { description: "StackMemory context storage", version: "1.0.0", created_at: (/* @__PURE__ */ new Date()).toISOString() } }); this.logger.info(`ChromaDB collection '${collectionName}' initialized`); } catch (error) { this.logger.error("Failed to initialize ChromaDB collection", error); throw error; } } /** * Store a frame in ChromaDB */ async storeFrame(frame) { if (!this.collection) { throw new Error("ChromaDB not initialized"); } try { const frameMetadata = { user_id: this.userId, frame_id: frame.frameId, session_id: frame.sessionId || "unknown", project_name: frame.projectName || "default", timestamp: frame.timestamp, type: "frame", score: frame.score, tags: frame.tags || [] }; if (this.teamId) { frameMetadata.team_id = this.teamId; } const document = { id: `frame_${frame.frameId}_${this.userId}`, document: this.frameToDocument(frame), metadata: frameMetadata }; await this.collection.add({ ids: [document.id], documents: [document.document], metadatas: [document.metadata] }); this.logger.debug( `Stored frame ${frame.frameId} for user ${this.userId}` ); } catch (error) { this.logger.error(`Failed to store frame ${frame.frameId}`, error); throw error; } } /** * Store a decision or observation */ async storeContext(type, content, metadata) { if (!this.collection) { throw new Error("ChromaDB not initialized"); } try { const contextId = `${type}_${uuidv4()}_${this.userId}`; const documentMetadata = { user_id: this.userId, frame_id: metadata?.frame_id || "none", session_id: metadata?.session_id || "unknown", project_name: metadata?.project_name || "default", timestamp: Date.now(), type, ...metadata }; if (this.teamId) { documentMetadata.team_id = this.teamId; } const document = { id: contextId, document: content, metadata: documentMetadata }; await this.collection.add({ ids: [document.id], documents: [document.document], metadatas: [document.metadata] }); this.logger.debug(`Stored ${type} for user ${this.userId}`); } catch (error) { this.logger.error(`Failed to store ${type}`, error); throw error; } } /** * Query contexts by semantic similarity */ async queryContexts(query, limit = 10, filters) { if (!this.collection) { throw new Error("ChromaDB not initialized"); } try { const whereClause = { user_id: this.userId }; if (this.teamId) { whereClause["$or"] = [ { team_id: this.teamId }, { user_id: this.userId } ]; } if (filters?.type && filters.type.length > 0) { whereClause.type = { $in: filters.type }; } if (filters?.projectName) { whereClause.project_name = filters.projectName; } if (filters?.sessionId) { whereClause.session_id = filters.sessionId; } if (filters?.startTime || filters?.endTime) { whereClause.timestamp = {}; if (filters.startTime) { whereClause.timestamp.$gte = filters.startTime; } if (filters.endTime) { whereClause.timestamp.$lte = filters.endTime; } } const results = await this.collection.query({ queryTexts: [query], nResults: limit, where: whereClause, include: ["documents", "metadatas", "distances"] }); const contexts = []; if (results.documents && results.documents[0]) { for (let i = 0; i < results.documents[0].length; i++) { contexts.push({ content: results.documents[0][i] || "", metadata: results.metadatas?.[0]?.[i] || {}, distance: results.distances?.[0]?.[i] || 0 }); } } this.logger.debug(`Found ${contexts.length} contexts for query`); return contexts; } catch (error) { this.logger.error("Failed to query contexts", error); throw error; } } /** * Get user's recent contexts */ async getRecentContexts(limit = 20, type) { if (!this.collection) { throw new Error("ChromaDB not initialized"); } try { const whereClause = { user_id: this.userId }; if (type) { whereClause.type = type; } const results = await this.collection.get({ where: whereClause, include: ["documents", "metadatas"] }); const contexts = []; if (results.documents) { const indexed = results.documents.map((doc, i) => ({ content: doc || "", metadata: results.metadatas?.[i] || {} })); indexed.sort( (a, b) => (b.metadata.timestamp || 0) - (a.metadata.timestamp || 0) ); contexts.push(...indexed.slice(0, limit)); } return contexts; } catch (error) { this.logger.error("Failed to get recent contexts", error); throw error; } } /** * Delete old contexts (retention policy) */ async deleteOldContexts(olderThanDays = 30) { if (!this.collection) { throw new Error("ChromaDB not initialized"); } try { const cutoffTime = Date.now() - olderThanDays * 24 * 60 * 60 * 1e3; const results = await this.collection.get({ where: { user_id: this.userId, timestamp: { $lt: cutoffTime } }, include: ["ids"] }); if (!results.ids || results.ids.length === 0) { return 0; } await this.collection.delete({ ids: results.ids }); this.logger.info(`Deleted ${results.ids.length} old contexts`); return results.ids.length; } catch (error) { this.logger.error("Failed to delete old contexts", error); throw error; } } /** * Get team contexts (if user is part of a team) */ async getTeamContexts(limit = 20) { if (!this.collection || !this.teamId) { return []; } try { const results = await this.collection.get({ where: { team_id: this.teamId }, include: ["documents", "metadatas"], limit }); const contexts = []; if (results.documents) { for (let i = 0; i < results.documents.length; i++) { contexts.push({ content: results.documents[i] || "", metadata: results.metadatas?.[i] || {} }); } } return contexts; } catch (error) { this.logger.error("Failed to get team contexts", error); return []; } } /** * Convert frame to searchable document */ frameToDocument(frame) { const parts = [ `Frame: ${frame.title}`, `Type: ${frame.type}`, `Status: ${frame.status}` ]; if (frame.description) { parts.push(`Description: ${frame.description}`); } if (frame.inputs && frame.inputs.length > 0) { parts.push(`Inputs: ${frame.inputs.join(", ")}`); } if (frame.outputs && frame.outputs.length > 0) { parts.push(`Outputs: ${frame.outputs.join(", ")}`); } if (frame.tags && frame.tags.length > 0) { parts.push(`Tags: ${frame.tags.join(", ")}`); } if (frame.digest_json) { try { const digest = JSON.parse(frame.digest_json); if (digest.summary) { parts.push(`Summary: ${digest.summary}`); } if (digest.keyDecisions) { parts.push(`Decisions: ${digest.keyDecisions.join(". ")}`); } } catch { } } return parts.join("\n"); } /** * Update team ID for a user */ async updateTeamId(newTeamId) { this.teamId = newTeamId; this.logger.info(`Updated team ID to ${newTeamId} for user ${this.userId}`); } /** * Get storage statistics */ async getStats() { if (!this.collection) { throw new Error("ChromaDB not initialized"); } try { const userResults = await this.collection.get({ where: { user_id: this.userId }, include: ["metadatas"] }); const stats = { totalDocuments: 0, userDocuments: userResults.ids?.length || 0, documentsByType: {} }; if (userResults.metadatas) { for (const metadata of userResults.metadatas) { const type = metadata?.type || "unknown"; stats.documentsByType[type] = (stats.documentsByType[type] || 0) + 1; } } if (this.teamId) { const teamResults = await this.collection.get({ where: { team_id: this.teamId }, include: ["ids"] }); stats.teamDocuments = teamResults.ids?.length || 0; } stats.totalDocuments = stats.userDocuments + (stats.teamDocuments || 0); return stats; } catch (error) { this.logger.error("Failed to get stats", error); throw error; } } } export { ChromaDBAdapter }; //# sourceMappingURL=chromadb-adapter.js.map