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.

457 lines (455 loc) 12.5 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { DEFAULT_DIFFMEM_CONFIG } from "../../diffmem/config.js"; import { logger } from "../../../core/monitoring/logger.js"; class DiffMemHandlers { config; cache = /* @__PURE__ */ new Map(); cacheTTL = 5 * 60 * 1e3; // 5 minutes constructor(deps) { this.config = { ...DEFAULT_DIFFMEM_CONFIG, ...deps?.config }; } /** * Get tool definitions for DiffMem tools */ getToolDefinitions() { return [ { name: "diffmem_get_user_context", description: "Fetch user knowledge and preferences from memory. Use to personalize responses based on learned user patterns.", inputSchema: { type: "object", properties: { categories: { type: "array", items: { type: "string", enum: [ "preference", "expertise", "project_knowledge", "pattern", "correction" ] }, description: "Filter by memory categories" }, limit: { type: "number", default: 10, description: "Maximum memories to return" } } } }, { name: "diffmem_store_learning", description: "Store a new insight about the user (preference, expertise, pattern, or correction)", inputSchema: { type: "object", properties: { content: { type: "string", description: "The insight to store" }, category: { type: "string", enum: [ "preference", "expertise", "project_knowledge", "pattern", "correction" ], description: "Category of the insight" }, confidence: { type: "number", minimum: 0, maximum: 1, default: 0.7, description: "Confidence level (0-1)" }, context: { type: "object", description: "Additional context for the insight" } }, required: ["content", "category"] } }, { name: "diffmem_search", description: "Semantic search across user memories. Find relevant past insights and preferences.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, timeRange: { type: "string", enum: ["day", "week", "month", "all"], default: "all", description: "Time range filter" }, minConfidence: { type: "number", minimum: 0, maximum: 1, default: 0.5, description: "Minimum confidence threshold" }, limit: { type: "number", default: 10, description: "Maximum results" } }, required: ["query"] } }, { name: "diffmem_status", description: "Check DiffMem connection status and memory statistics", inputSchema: { type: "object", properties: {} } } ]; } /** * Fetch user context/memories with optional category filter */ async handleGetUserContext(args) { const { categories, limit = 10 } = args; try { const cacheKey = `context:${JSON.stringify(categories)}:${limit}`; const cached = this.getFromCache(cacheKey); if (cached) { logger.debug("DiffMem cache hit", { cacheKey }); return this.formatMemoriesResponse(cached, true); } const query = { limit }; if (categories?.length) { query.categories = categories; } const memories = await this.fetchMemories(query); this.setCache(cacheKey, memories); return this.formatMemoriesResponse(memories, false); } catch (error) { return this.handleError("getUserContext", error); } } /** * Store a new learning/insight */ async handleStoreLearning(args) { const { content, category, confidence = 0.7, context } = args; if (!content) { return { content: [{ type: "text", text: "Error: content is required" }], metadata: { error: true } }; } if (!category) { return { content: [{ type: "text", text: "Error: category is required" }], metadata: { error: true } }; } try { const insight = { content, category, confidence, source: "stackmemory", timestamp: Date.now(), context }; await this.storeInsight(insight); this.invalidateCacheByPrefix("context:"); logger.info("Stored DiffMem insight", { category, confidence }); return { content: [ { type: "text", text: `Stored ${category} insight: "${content.substring(0, 50)}${content.length > 50 ? "..." : ""}"` } ], metadata: { stored: true, category, confidence } }; } catch (error) { return this.handleError("storeLearning", error); } } /** * Semantic search across memories */ async handleSearch(args) { const { query, timeRange = "all", minConfidence = 0.5, limit = 10 } = args; if (!query) { return { content: [{ type: "text", text: "Error: query is required" }], metadata: { error: true } }; } try { const searchQuery = { query, timeRange, minConfidence, limit }; const results = await this.searchMemories(searchQuery); if (results.length === 0) { return { content: [ { type: "text", text: `No memories found matching "${query}"` } ], metadata: { query, resultCount: 0 } }; } const formattedResults = results.map( (m, i) => `${i + 1}. [${m.category}] ${m.content} (confidence: ${(m.confidence * 100).toFixed(0)}%)` ).join("\n"); return { content: [ { type: "text", text: `Search results for "${query}": ${formattedResults}` } ], metadata: { query, resultCount: results.length, results: results.map((m) => ({ id: m.id, category: m.category, confidence: m.confidence })) } }; } catch (error) { return this.handleError("search", error); } } /** * Get DiffMem connection status */ async handleStatus() { try { const status = await this.getStatus(); const statusText = status.connected ? `DiffMem Status: - Connected: Yes - Memories: ${status.memoryCount} - Last sync: ${status.lastSync ? new Date(status.lastSync).toISOString() : "Never"} - Version: ${status.version || "Unknown"}` : `DiffMem Status: - Connected: No - Endpoint: ${this.config.endpoint} - Enabled: ${this.config.enabled}`; return { content: [{ type: "text", text: statusText }], metadata: status }; } catch (error) { return this.handleError("status", error); } } // Private helper methods formatMemoriesResponse(memories, fromCache) { if (memories.length === 0) { return { content: [ { type: "text", text: "No user context memories found." } ], metadata: { fromCache, count: 0 } }; } const byCategory = memories.reduce( (acc, m) => { if (!acc[m.category]) { acc[m.category] = []; } acc[m.category].push(m); return acc; }, {} ); const sections = Object.entries(byCategory).map(([category, mems]) => { const items = mems.map( (m) => ` - ${m.content} (${(m.confidence * 100).toFixed(0)}% confidence)` ).join("\n"); return `${category.toUpperCase()}: ${items}`; }).join("\n\n"); return { content: [ { type: "text", text: `User Context: ${sections}` } ], metadata: { fromCache, count: memories.length, categories: Object.keys(byCategory) } }; } handleError(operation, error) { const errorMessage = error instanceof Error ? error.message : String(error); const isConnectionError = errorMessage.includes("ECONNREFUSED") || errorMessage.includes("fetch failed") || errorMessage.includes("network"); logger.warn(`DiffMem ${operation} failed`, { error: errorMessage }); if (isConnectionError) { return { content: [ { type: "text", text: `DiffMem unavailable (${operation}). Session continues without user memory.` } ], metadata: { error: true, unavailable: true, operation } }; } return { content: [ { type: "text", text: `DiffMem ${operation} error: ${errorMessage}` } ], metadata: { error: true, operation, message: errorMessage } }; } getFromCache(key) { const entry = this.cache.get(key); if (!entry) return void 0; if (Date.now() - entry.timestamp > this.cacheTTL) { this.cache.delete(key); return void 0; } return entry.data; } setCache(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); } invalidateCacheByPrefix(prefix) { for (const key of this.cache.keys()) { if (key.startsWith(prefix)) { this.cache.delete(key); } } } // API interaction methods (with graceful degradation) async fetchMemories(query) { if (!this.config.enabled) { return []; } const response = await fetch(`${this.config.endpoint}/memories`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(query), signal: AbortSignal.timeout(this.config.timeout) }); if (!response.ok) { throw new Error(`DiffMem API error: ${response.status}`); } const data = await response.json(); return data.memories || []; } async searchMemories(query) { if (!this.config.enabled) { return []; } const response = await fetch(`${this.config.endpoint}/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(query), signal: AbortSignal.timeout(this.config.timeout) }); if (!response.ok) { throw new Error(`DiffMem API error: ${response.status}`); } const data = await response.json(); return data.results || []; } async storeInsight(insight) { if (!this.config.enabled) { logger.debug("DiffMem disabled, skipping store"); return; } const response = await fetch(`${this.config.endpoint}/insights`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(insight), signal: AbortSignal.timeout(this.config.timeout) }); if (!response.ok) { throw new Error(`DiffMem API error: ${response.status}`); } } async getStatus() { if (!this.config.enabled) { return { connected: false, memoryCount: 0, lastSync: null }; } try { const response = await fetch(`${this.config.endpoint}/status`, { method: "GET", signal: AbortSignal.timeout(this.config.timeout) }); if (!response.ok) { return { connected: false, memoryCount: 0, lastSync: null }; } return await response.json(); } catch { return { connected: false, memoryCount: 0, lastSync: null }; } } } export { DiffMemHandlers }; //# sourceMappingURL=diffmem-handlers.js.map