UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

209 lines (208 loc) 5.89 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 "./config.js"; class DiffMemClientError extends Error { constructor(message, code, statusCode) { super(message); this.code = code; this.statusCode = statusCode; this.name = "DiffMemClientError"; } } class DiffMemClient { endpoint; userId; timeout; maxRetries; constructor(config = {}) { const mergedConfig = { ...DEFAULT_DIFFMEM_CONFIG, ...config }; this.endpoint = mergedConfig.endpoint.replace(/\/$/, ""); this.userId = mergedConfig.userId; this.timeout = mergedConfig.timeout; this.maxRetries = mergedConfig.maxRetries; } async request(path, options = {}) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); let lastError; for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { const response = await fetch(`${this.endpoint}${path}`, { ...options, signal: controller.signal, headers: { "Content-Type": "application/json", ...options.headers } }); clearTimeout(timeoutId); if (!response.ok) { const _errorBody = await response.text().catch(() => ""); throw new DiffMemClientError( `Request failed: ${response.statusText}`, "HTTP_ERROR", response.status ); } return await response.json(); } catch (error) { lastError = error; if (error instanceof DiffMemClientError) { throw error; } if (error.name === "AbortError") { throw new DiffMemClientError("Request timeout", "TIMEOUT"); } if (attempt < this.maxRetries) { await new Promise( (resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100) ); continue; } } } clearTimeout(timeoutId); throw new DiffMemClientError( lastError?.message || "Request failed after retries", "NETWORK_ERROR" ); } /** * Get user context/memories from DiffMem * Maps to POST /memory/{user_id}/context */ async getMemories(query = {}) { try { const conversation = query.query ? [{ role: "user", content: query.query }] : [{ role: "user", content: "What do you know about me?" }]; const response = await this.request(`/memory/${this.userId}/context`, { method: "POST", body: JSON.stringify({ conversation, depth: "wide" }) }); if (response.entities) { return response.entities.slice(0, query.limit || 10).map((entity) => ({ id: entity.id, content: entity.content, category: "project_knowledge", confidence: entity.score || 0.7, timestamp: Date.now() })); } return []; } catch { return []; } } /** * Store an insight/learning in DiffMem * Maps to POST /memory/{user_id}/process-and-commit */ async storeInsight(insight) { const response = await this.request(`/memory/${this.userId}/process-and-commit`, { method: "POST", body: JSON.stringify({ memory_input: `[${insight.category}] ${insight.content}`, session_id: `sm-${Date.now()}`, session_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] }) }); return { id: response.session_id || `insight-${Date.now()}` }; } /** * Search memories in DiffMem * Maps to POST /memory/{user_id}/search */ async search(query) { try { const response = await this.request(`/memory/${this.userId}/search`, { method: "POST", body: JSON.stringify({ query: query.query || "", k: query.limit || 10 }) }); if (response.results) { return response.results.map((result) => ({ id: result.snippet.id, content: result.snippet.content, category: "project_knowledge", confidence: result.score, timestamp: Date.now(), metadata: { filePath: result.snippet.file_path } })); } return []; } catch { return []; } } /** * Get DiffMem server status * Maps to GET /health */ async getStatus() { try { const health = await this.request("/health", { method: "GET" }); return { connected: health.status === "healthy", memoryCount: health.active_contexts || 0, lastSync: Date.now(), version: health.version }; } catch { return { connected: false, memoryCount: 0, lastSync: null }; } } /** * Batch sync multiple insights * Processes each insight individually since DiffMem doesn't have batch API */ async batchSync(insights) { if (insights.length === 0) { return { synced: 0, failed: 0 }; } let synced = 0; let failed = 0; for (const insight of insights) { try { await this.storeInsight(insight); synced++; } catch { failed++; } } return { synced, failed }; } /** * Onboard a new user in DiffMem */ async onboardUser(userInfo) { try { const response = await this.request( `/memory/${this.userId}/onboard`, { method: "POST", body: JSON.stringify({ user_info: userInfo, session_id: `onboard-${Date.now()}` }) } ); return { success: response.status === "success" }; } catch { return { success: false }; } } } export { DiffMemClient, DiffMemClientError };