@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.
210 lines (209 loc) • 5.93 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 { 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
};
//# sourceMappingURL=client.js.map