@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
JavaScript
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