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:
Elliot Chen
2026-06-05 22:35:51 +08:00
commit 518b8eca85
636 changed files with 160553 additions and 0 deletions

View 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;
}