chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
This commit is contained in:
280
use-cases/claude-code-plugin/hooks/scripts/utils/evermem-api.js
Normal file
280
use-cases/claude-code-plugin/hooks/scripts/utils/evermem-api.js
Normal file
@ -0,0 +1,280 @@
|
||||
/**
|
||||
* EverMem Cloud API client
|
||||
* Handles memory search and storage operations
|
||||
*/
|
||||
|
||||
import { getConfig } from './config.js';
|
||||
import { debug, setDebugPrefix } from './debug.js';
|
||||
|
||||
// Set debug prefix for this script
|
||||
setDebugPrefix('EverMemAPI');
|
||||
const TIMEOUT_MS = 30000; // 30 seconds
|
||||
|
||||
/**
|
||||
* Search memories from EverMem Cloud (v1)
|
||||
* @param {string} query - Search query text
|
||||
* @param {Object} options - Additional options
|
||||
* @param {number} options.topK - Max results (default: 10)
|
||||
* @param {string} options.retrieveMethod - Search method: keyword|vector|hybrid|agentic (default: 'hybrid')
|
||||
* @param {string[]} options.memoryTypes - Memory types (default: ['episodic_memory'])
|
||||
* @returns {Promise<Object>} Raw API response with _debug envelope
|
||||
*/
|
||||
export async function searchMemories(query, options = {}) {
|
||||
const config = getConfig();
|
||||
|
||||
if (!config.isConfigured) {
|
||||
throw new Error('EverMem API key not configured');
|
||||
}
|
||||
|
||||
const {
|
||||
topK = 10,
|
||||
retrieveMethod = 'hybrid',
|
||||
memoryTypes = ['episodic_memory']
|
||||
} = options;
|
||||
|
||||
const url = `${config.apiBaseUrl}/api/v1/memories/search`;
|
||||
const filters = config.groupId
|
||||
? { group_id: config.groupId }
|
||||
: { user_id: config.userId };
|
||||
|
||||
const requestBody = {
|
||||
query,
|
||||
method: retrieveMethod,
|
||||
top_k: topK,
|
||||
memory_types: memoryTypes,
|
||||
filters
|
||||
};
|
||||
|
||||
debug('searchMemories request body', requestBody);
|
||||
|
||||
const debugEnvelope = {
|
||||
url,
|
||||
requestBody,
|
||||
apiKeyMasked: 'API_KEY_HIDDEN'
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const text = await response.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
return { _debug: { ...debugEnvelope, status: response.status, rawBody: text, error: 'non-JSON response' } };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return { _debug: { ...debugEnvelope, status: response.status, error: data } };
|
||||
}
|
||||
|
||||
data._debug = debugEnvelope;
|
||||
return data;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error(`API timeout after ${TIMEOUT_MS}ms`);
|
||||
}
|
||||
return { _debug: { ...debugEnvelope, error: error.message } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform v1 search API response to plugin memory format.
|
||||
* v1 returns: { data: { episodes: [{ id, user_id, session_id, timestamp, summary, subject, score, participants, group_id? }], ... } }
|
||||
* @param {Object} apiResponse - Raw v1 API response
|
||||
* @returns {Object[]} Formatted memories sorted by score desc
|
||||
*/
|
||||
export function transformSearchResults(apiResponse) {
|
||||
const episodes = apiResponse?.data?.episodes;
|
||||
if (!Array.isArray(episodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const memories = [];
|
||||
for (const ep of episodes) {
|
||||
const content = ep.summary || '';
|
||||
if (!content) continue;
|
||||
|
||||
memories.push({
|
||||
text: content,
|
||||
subject: ep.subject || '',
|
||||
timestamp: ep.timestamp || new Date().toISOString(),
|
||||
memoryType: ep.memory_type || 'episodic_memory',
|
||||
score: ep.score || 0,
|
||||
metadata: {
|
||||
groupId: ep.group_id,
|
||||
type: ep.memory_type,
|
||||
participants: ep.participants
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
memories.sort((a, b) => b.score - a.score);
|
||||
return memories;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a memory to EverMem Cloud (v1).
|
||||
* Uses /api/v1/memories/group when config.groupId is set, else /api/v1/memories (personal).
|
||||
* @param {Object} message - Message to store
|
||||
* @param {string} message.content - Message content
|
||||
* @param {string} message.role - 'user' or 'assistant'
|
||||
* @param {string} [message.messageId] - (unused in v1; accepted for backward compatibility)
|
||||
* @returns {Promise<Object>} Debug envelope { url, body, status, ok, response }
|
||||
*/
|
||||
export async function addMemory(message) {
|
||||
const config = getConfig();
|
||||
|
||||
if (!config.isConfigured) {
|
||||
throw new Error('EverMem API key not configured');
|
||||
}
|
||||
|
||||
const role = message.role === 'assistant' ? 'assistant' : 'user';
|
||||
const sender_id = role === 'assistant' ? 'claude-assistant' : config.userId;
|
||||
|
||||
const baseMessage = {
|
||||
sender_id,
|
||||
role,
|
||||
timestamp: Date.now(),
|
||||
content: message.content
|
||||
};
|
||||
|
||||
let url;
|
||||
let requestBody;
|
||||
|
||||
if (config.groupId) {
|
||||
url = `${config.apiBaseUrl}/api/v1/memories/group`;
|
||||
requestBody = {
|
||||
group_id: config.groupId,
|
||||
messages: [baseMessage],
|
||||
async_mode: true
|
||||
};
|
||||
} else {
|
||||
url = `${config.apiBaseUrl}/api/v1/memories`;
|
||||
requestBody = {
|
||||
user_id: config.userId,
|
||||
messages: [baseMessage],
|
||||
async_mode: true
|
||||
};
|
||||
}
|
||||
|
||||
let response, responseText, responseData, status, ok;
|
||||
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
status = response.status;
|
||||
ok = response.ok;
|
||||
responseText = await response.text();
|
||||
try {
|
||||
responseData = JSON.parse(responseText);
|
||||
} catch {}
|
||||
} catch (fetchError) {
|
||||
status = 0;
|
||||
ok = false;
|
||||
responseText = fetchError.message;
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
body: requestBody,
|
||||
status,
|
||||
ok,
|
||||
response: responseData || responseText
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memories from EverMem Cloud (v1, ordered newest first by default).
|
||||
* @param {Object} options - Options
|
||||
* @param {number} options.page - Page number (default: 1)
|
||||
* @param {number} options.pageSize - Results per page (default: 100, max: 100)
|
||||
* @param {string} options.memoryType - Memory type filter (default: 'episodic_memory')
|
||||
* @returns {Promise<Object>} Raw v1 response { data: { episodes, total_count, count, ... } }
|
||||
*/
|
||||
export async function getMemories(options = {}) {
|
||||
const config = getConfig();
|
||||
|
||||
if (!config.isConfigured) {
|
||||
throw new Error('EverMem API key not configured');
|
||||
}
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 100,
|
||||
memoryType = 'episodic_memory'
|
||||
} = options;
|
||||
|
||||
const filters = config.groupId
|
||||
? { group_id: config.groupId }
|
||||
: { user_id: config.userId };
|
||||
|
||||
const url = `${config.apiBaseUrl}/api/v1/memories/get`;
|
||||
const requestBody = {
|
||||
memory_type: memoryType,
|
||||
filters,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
rank_by: 'timestamp',
|
||||
rank_order: 'desc'
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`API error ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform v1 getMemories response to simple format.
|
||||
* @param {Object} apiResponse - Raw v1 API response
|
||||
* @returns {Object[]} Formatted memories newest-first
|
||||
*/
|
||||
export function transformGetMemoriesResults(apiResponse) {
|
||||
const episodes = apiResponse?.data?.episodes;
|
||||
if (!Array.isArray(episodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const memories = episodes.map(ep => ({
|
||||
text: ep.episode || ep.summary || '',
|
||||
subject: ep.subject || '',
|
||||
timestamp: ep.timestamp || new Date().toISOString(),
|
||||
groupId: ep.group_id
|
||||
})).filter(m => m.text);
|
||||
|
||||
memories.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||
return memories;
|
||||
}
|
||||
Reference in New Issue
Block a user