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:
224
use-cases/claude-code-plugin/mcp/server.js
Executable file
224
use-cases/claude-code-plugin/mcp/server.js
Executable file
@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* EverMem MCP Server
|
||||
* Exposes memory search tool for Claude to find relevant context from past sessions
|
||||
*/
|
||||
|
||||
import { createInterface } from 'readline';
|
||||
import { searchMemories, transformSearchResults } from '../hooks/scripts/utils/evermem-api.js';
|
||||
import { getConfig } from '../hooks/scripts/utils/config.js';
|
||||
|
||||
// Tool definitions - following claude-mem's concise pattern
|
||||
const TOOLS = [
|
||||
{
|
||||
name: 'evermem_search',
|
||||
description: 'Search past conversation memories. Returns summaries with dates and relevance scores. Use when user asks about previous work, decisions, or context from past sessions. Params: query (required), limit (default: 10, max: 20)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query - use keywords, topics, or questions'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Max results to return (default: 10, max: 20)'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Format date as relative time (e.g., "2 days ago", "today")
|
||||
*/
|
||||
function formatRelativeDate(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return 'today';
|
||||
if (diffDays === 1) return 'yesterday';
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle evermem_search tool call
|
||||
*/
|
||||
async function handleSearch(args) {
|
||||
const config = getConfig();
|
||||
|
||||
if (!config.isConfigured) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: 'EverMem API key not configured. Set EVERMEM_API_KEY environment variable.' }]
|
||||
};
|
||||
}
|
||||
|
||||
const query = args.query;
|
||||
if (!query) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: 'Missing required parameter: query' }]
|
||||
};
|
||||
}
|
||||
|
||||
const limit = Math.min(args.limit || 10, 20);
|
||||
|
||||
try {
|
||||
const response = await searchMemories(query, { topK: limit });
|
||||
const memories = transformSearchResults(response);
|
||||
|
||||
if (memories.length === 0) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `No memories found for: "${query}"` }]
|
||||
};
|
||||
}
|
||||
|
||||
// Format as compact table (token-efficient like claude-mem)
|
||||
const header = '| # | Score | Date | Summary |';
|
||||
const separator = '|---|-------|------|---------|';
|
||||
|
||||
const rows = memories.map((mem, i) => {
|
||||
const score = Math.round(mem.score * 100);
|
||||
const date = formatRelativeDate(mem.timestamp);
|
||||
// Use full subject field
|
||||
const summary = (mem.subject || mem.text.substring(0, 150)).replace(/\|/g, '/').replace(/\n/g, ' ');
|
||||
return `| ${i + 1} | ${score}% | ${date} | ${summary} |`;
|
||||
});
|
||||
|
||||
const table = [header, separator, ...rows].join('\n');
|
||||
|
||||
// Add context about what was found
|
||||
const resultText = `Found ${memories.length} memories for "${query}":\n\n${table}\n\nTo get full content of a specific memory, ask me to elaborate on that topic.`;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: resultText }]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: `Search error: ${error.message}` }]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming JSON-RPC request
|
||||
*/
|
||||
async function handleRequest(request) {
|
||||
const { id, method, params } = request;
|
||||
|
||||
switch (method) {
|
||||
case 'initialize':
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {}
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'evermem',
|
||||
version: '0.1.0'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
case 'notifications/initialized':
|
||||
return null;
|
||||
|
||||
case 'tools/list':
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
tools: TOOLS
|
||||
}
|
||||
};
|
||||
|
||||
case 'tools/call':
|
||||
const { name, arguments: args } = params;
|
||||
let result;
|
||||
|
||||
switch (name) {
|
||||
case 'evermem_search':
|
||||
result = await handleSearch(args || {});
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Unknown tool: ${name}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Method not found: ${method}`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main MCP server loop
|
||||
*/
|
||||
async function main() {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
rl.on('line', async (line) => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
try {
|
||||
const request = JSON.parse(line);
|
||||
const response = await handleRequest(request);
|
||||
|
||||
if (response) {
|
||||
console.log(JSON.stringify(response));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorResponse = {
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: {
|
||||
code: -32700,
|
||||
message: `Parse error: ${error.message}`
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(errorResponse));
|
||||
}
|
||||
});
|
||||
|
||||
rl.on('close', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('MCP server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user