@jeanmemory/node
Version:
Node.js SDK for Jean Memory - Power your Next.js and other Node.js backends with a perfect memory
329 lines • 12.8 kB
JavaScript
"use strict";
/**
* Jean Memory Node.js SDK Client
* Main client for interacting with Jean Memory API
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.JeanMemoryClient = exports.JeanMemoryError = void 0;
const auth_1 = require("./auth");
const mcp_1 = require("./mcp");
class JeanMemoryError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = 'JeanMemoryError';
}
}
exports.JeanMemoryError = JeanMemoryError;
class JeanMemoryClient {
constructor(config) {
/**
* Direct tool access namespace with OAuth support
*/
this.tools = {
add_memory: async (params) => {
// Handle both signatures for flexibility
const isObject = typeof params === 'object';
const content = isObject ? params.content : params;
const userToken = isObject && params.user_token ? params.user_token : await this.getTestUserToken();
const mcpResponse = await (0, mcp_1.makeMCPRequest)(userToken, this.apiKey, 'add_memories', {
text: content,
tags: [],
priority: false
}, this.apiBase);
if (mcpResponse.error) {
throw new JeanMemoryError(mcpResponse.error.message, mcpResponse.error.code);
}
return mcpResponse.result;
},
search_memory: async (params) => {
// Handle both signatures for flexibility
const isObject = typeof params === 'object';
const query = isObject ? params.query : params;
const userToken = isObject && params.user_token ? params.user_token : await this.getTestUserToken();
const mcpResponse = await (0, mcp_1.makeMCPRequest)(userToken, this.apiKey, 'search_memory', {
query: query,
limit: 10,
tags_filter: null,
deep_search: false
}, this.apiBase);
if (mcpResponse.error) {
throw new JeanMemoryError(mcpResponse.error.message, mcpResponse.error.code);
}
return mcpResponse.result;
},
deep_memory_query: async (params) => {
// Handle both signatures for flexibility
const isObject = typeof params === 'object';
const query = isObject ? params.query : params;
const userToken = isObject && params.user_token ? params.user_token : await this.getTestUserToken();
const mcpResponse = await (0, mcp_1.makeMCPRequest)(userToken, this.apiKey, 'deep_memory_query', {
query: query
}, this.apiBase);
if (mcpResponse.error) {
throw new JeanMemoryError(mcpResponse.error.message, mcpResponse.error.code);
}
return mcpResponse.result;
},
store_document: async (params) => {
const { user_token, title, content, document_type = 'markdown' } = params;
const userToken = user_token || await this.getTestUserToken();
const mcpResponse = await (0, mcp_1.makeMCPRequest)(userToken, this.apiKey, 'store_document', {
title,
content,
document_type
}, this.apiBase);
if (mcpResponse.error) {
throw new JeanMemoryError(mcpResponse.error.message, mcpResponse.error.code);
}
return mcpResponse.result;
}
};
if (!config.apiKey) {
throw new Error('API key is required');
}
if (!config.apiKey.startsWith('jean_sk_')) {
throw new Error('Invalid API key format. Must start with "jean_sk_"');
}
this.apiKey = config.apiKey;
this.apiBase = config.apiBase || 'https://jean-memory-api-virginia.onrender.com';
this.userAgent = config.userAgent || 'JeanMemory-Node-SDK/1.2.4';
}
/**
* Make authenticated HTTP request to Jean Memory API
*/
async makeRequest(method, endpoint, data) {
const url = new URL(endpoint, this.apiBase).toString();
const options = {
method,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': this.userAgent,
},
};
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
options.body = JSON.stringify(data);
}
else if (data && method === 'GET') {
const searchParams = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
});
const urlWithParams = new URL(url);
urlWithParams.search = searchParams.toString();
const finalUrl = urlWithParams.toString();
const response = await fetch(finalUrl, options);
return this.handleResponse(response);
}
const response = await fetch(url, options);
return this.handleResponse(response);
}
/**
* Handle HTTP response and error cases
*/
async handleResponse(response) {
if (!response.ok) {
let errorMessage = `Request failed with status ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.message || errorMessage;
}
catch {
// If JSON parsing fails, use status text
errorMessage = response.statusText || errorMessage;
}
throw new JeanMemoryError(errorMessage, response.status);
}
try {
return await response.json();
}
catch {
throw new JeanMemoryError('Invalid JSON response');
}
}
/**
* Store a new memory
*/
async storeMemory(content, context) {
if (!content || !content.trim()) {
throw new Error('Content cannot be empty');
}
const request = {
content: content.trim(),
context: context || {}
};
return this.makeRequest('POST', '/api/v1/memories', request);
}
/**
* Retrieve memories based on search query
*/
async retrieveMemories(query, options = {}) {
if (!query || !query.trim()) {
throw new Error('Query cannot be empty');
}
const { limit = 10, offset = 0 } = options;
if (limit < 1 || limit > 100) {
throw new Error('Limit must be between 1 and 100');
}
if (offset < 0) {
throw new Error('Offset must be non-negative');
}
const params = {
query: query.trim(),
limit,
offset
};
const response = await this.makeRequest('GET', '/api/v1/memories/search', params);
return response.data?.memories || [];
}
/**
* Get formatted context for a query (legacy method)
*/
async getContextLegacy(query) {
if (!query || !query.trim()) {
throw new Error('Query cannot be empty');
}
const memories = await this.retrieveMemories(query, { limit: 5 });
if (memories.length === 0) {
return 'No relevant context found.';
}
const contextParts = memories.map((memory, index) => {
const content = memory.content || '';
const timestamp = memory.created_at || '';
return `${index + 1}. ${content} (${timestamp})`;
});
return 'Relevant context:\\n' + contextParts.join('\\n');
}
/**
* Get or create auto test user for this API key
*/
async getTestUserToken() {
try {
const response = await fetch(`${this.apiBase}/api/v1/test-user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': this.userAgent
}
});
if (!response.ok) {
throw new Error(`Failed to get test user: ${response.statusText}`);
}
const data = await response.json();
// Add null checking for the response data
if (!data || !data.user_token) {
throw new Error('Invalid test user response: missing user_token');
}
return data.user_token;
}
catch (error) {
throw new JeanMemoryError(`Failed to create test user: ${error?.message || 'Unknown error'}`);
}
}
async getContext(paramsOrQuery) {
// Handle backward compatibility - simple string query
if (typeof paramsOrQuery === 'string') {
const userToken = await this.getTestUserToken();
const mcpResponse = await (0, mcp_1.makeMCPRequest)(userToken, this.apiKey, 'jean_memory', {
user_message: paramsOrQuery,
is_new_conversation: false,
needs_context: true
}, this.apiBase);
if (mcpResponse.error) {
throw new JeanMemoryError(mcpResponse.error.message, mcpResponse.error.code);
}
return mcpResponse.result?.content?.[0]?.text || '';
}
// Handle OAuth API - object with user_token and message
const { user_token, message, speed = 'balanced', tool = 'jean_memory', format = 'enhanced' } = paramsOrQuery;
const mcpResponse = await (0, mcp_1.makeMCPRequest)(user_token, this.apiKey, tool, {
user_message: message,
is_new_conversation: false,
needs_context: true,
speed,
format
}, this.apiBase);
if (mcpResponse.error) {
throw new JeanMemoryError(mcpResponse.error.message, mcpResponse.error.code);
}
// Return ContextResponse object
return {
text: mcpResponse.result?.content?.[0]?.text || '',
metadata: mcpResponse.result
};
}
/**
* List all memories with pagination
*/
async listMemories(options = {}) {
const { limit = 20, offset = 0 } = options;
if (limit < 1 || limit > 100) {
throw new Error('Limit must be between 1 and 100');
}
if (offset < 0) {
throw new Error('Offset must be non-negative');
}
const params = { limit, offset };
const response = await this.makeRequest('GET', '/api/v1/memories', params);
return {
memories: response.data?.memories || [],
total: response.data?.total || 0,
hasNext: response.data?.pagination?.has_next || false,
hasPrev: response.data?.pagination?.has_prev || false
};
}
/**
* Delete a specific memory
*/
async deleteMemory(memoryId) {
if (!memoryId) {
throw new Error('Memory ID is required');
}
await this.makeRequest('DELETE', `/api/v1/memories/${memoryId}`);
}
/**
* Stream memories (for large result sets)
*/
async streamMemories(query) {
if (!query || !query.trim()) {
throw new Error('Query cannot be empty');
}
// For now, implement as a batch fetch with streaming interface
// This can be enhanced when backend supports true streaming
const memories = await this.retrieveMemories(query, { limit: 100 });
return new ReadableStream({
start(controller) {
memories.forEach(memory => controller.enqueue(memory));
controller.close();
}
});
}
/**
* Check API health and authentication
*/
async healthCheck() {
return this.makeRequest('GET', '/api/v1/health');
}
/**
* Get current user information
*/
async getCurrentUser() {
return this.makeRequest('GET', '/api/v1/user/me');
}
/**
* Get authentication helper for OAuth flows
*/
createAuth(config) {
return new auth_1.JeanMemoryAuth({
apiKey: this.apiKey,
oauthBase: config?.oauthBase,
redirectPort: config?.redirectPort
});
}
}
exports.JeanMemoryClient = JeanMemoryClient;
//# sourceMappingURL=client.js.map