@everme/claude-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Plugin config loader.
3
+ *
4
+ * Source precedence:
5
+ *
6
+ * For EVERME_AGENT_TOKEN and EVERME_AGENT_ID — the per-machine
7
+ * credentials evercli rotates — `~/.claude/everme.env` always wins.
8
+ * evercli is the canonical owner of these values; if anything else
9
+ * (a stale .claude.json mcp.env block, a leftover shell var) has a
10
+ * different value, it's stale and the freshly-rotated evt must win.
11
+ *
12
+ * For every other EVERME_* (EVERME_API_KEY for emk-mode debugging,
13
+ * EVERME_API_BASE for self-hosted EverMe, …) process.env still wins:
14
+ * users may legitimately want to override these from a shell or from
15
+ * Claude Code's mcp .env block, and evercli does not own them.
16
+ *
17
+ * Compiled defaults (everme.evermind.ai, no token) sit at the bottom.
18
+ *
19
+ * Auth modes (mutually exclusive, both wire-compatible):
20
+ * evt — set EVERME_AGENT_TOKEN (per-machine token from evercli)
21
+ * emk — set EVERME_API_KEY (account-level, from EverMe Web UI)
22
+ *
23
+ * If neither is set the plugin runs in disabled-mode: hooks short-
24
+ * circuit silently so the host (Claude Code) is never blocked.
25
+ */
26
+
27
+ import { readFileSync, existsSync } from "node:fs";
28
+ import { homedir } from "node:os";
29
+ import { join } from "node:path";
30
+ import { resolveConfig as sdkResolveConfig } from "@everme/agent-sdk";
31
+
32
+ // Env-file location. EVERME_ENV_FILE_PATH overrides for tests so they
33
+ // don't get polluted by a real file on the developer's box.
34
+ function evermeEnvFilePath() {
35
+ return process.env.EVERME_ENV_FILE_PATH || join(homedir(), ".claude", "everme.env");
36
+ }
37
+
38
+ let cached = null;
39
+ let envFileLoaded = false;
40
+
41
+ // Keys that evercli rotates per machine. For these, the env file is
42
+ // the canonical source — if process.env carries a different value
43
+ // (stale .claude.json mcp.env block, leftover shell export from a
44
+ // previous account), the env file's value MUST overwrite it. Without
45
+ // this the freshly-rotated evt could be shadowed by a stale token,
46
+ // leaving every memory call 401.
47
+ const EVERME_ROTATED_KEYS = new Set([
48
+ "EVERME_AGENT_TOKEN",
49
+ "EVERME_AGENT_ID",
50
+ ]);
51
+
52
+ /**
53
+ * Load ~/.claude/everme.env (KEY=value lines) into process.env.
54
+ * Idempotent — runs once per process.
55
+ *
56
+ * For EVERME_ROTATED_KEYS the env file always wins. For everything
57
+ * else (EVERME_API_KEY, EVERME_API_BASE, …) process.env wins so users
58
+ * can override via shell or mcp .env block.
59
+ *
60
+ * This is the path evercli uses to hand the freshly-minted evt to the
61
+ * plugin without editing the user's shell profile (which is brittle:
62
+ * profile name varies by shell, and a user might run `claude` from a
63
+ * non-interactive shell that doesn't load .zshrc).
64
+ */
65
+ function loadEnvFile() {
66
+ if (envFileLoaded) return;
67
+ envFileLoaded = true;
68
+ const path = evermeEnvFilePath();
69
+ if (!existsSync(path)) return;
70
+ try {
71
+ const raw = readFileSync(path, "utf8");
72
+ for (const line of raw.split("\n")) {
73
+ const t = line.trim();
74
+ if (!t || t.startsWith("#")) continue;
75
+ const eq = t.indexOf("=");
76
+ if (eq < 1) continue;
77
+ const k = t.slice(0, eq).trim();
78
+ let v = t.slice(eq + 1).trim();
79
+ // Tolerate quoted values (single or double).
80
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
81
+ v = v.slice(1, -1);
82
+ }
83
+ if (EVERME_ROTATED_KEYS.has(k)) {
84
+ // evercli owns this key — env file always wins.
85
+ process.env[k] = v;
86
+ } else if (!process.env[k]) {
87
+ // user-overridable key — fill gap only.
88
+ process.env[k] = v;
89
+ }
90
+ }
91
+ } catch {
92
+ /* unreadable file is not fatal — plugin runs disabled */
93
+ }
94
+ }
95
+
96
+ export function getConfig() {
97
+ if (cached) return cached;
98
+ loadEnvFile();
99
+
100
+ const agentToken = process.env.EVERME_AGENT_TOKEN || process.env.EVERME_API_KEY || "";
101
+ const authMode = process.env.EVERME_AGENT_TOKEN
102
+ ? "evt"
103
+ : process.env.EVERME_API_KEY
104
+ ? "emk"
105
+ : "none";
106
+
107
+ const sdkCfg = sdkResolveConfig({
108
+ apiBase: process.env.EVERME_API_BASE,
109
+ agentId: process.env.EVERME_AGENT_ID,
110
+ agentToken,
111
+ topK: 5,
112
+ });
113
+
114
+ cached = {
115
+ ...sdkCfg,
116
+ authMode,
117
+ isConfigured: !!agentToken,
118
+ };
119
+ return cached;
120
+ }
121
+
122
+ export function isConfigured() {
123
+ return getConfig().isConfigured;
124
+ }
125
+
126
+ export function _resetCache() {
127
+ cached = null;
128
+ envFileLoaded = false;
129
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Renderers for the gateway's /mem/context profile snapshot.
3
+ *
4
+ * Profile shape (from EverMe gateway):
5
+ * { explicit_info: [{ category, description, evidence?, sources?[] }],
6
+ * implicit_traits: [{ trait, description, basis?, evidence? }],
7
+ * scenario, memcell_count, ... }
8
+ *
9
+ * `renderProfileBlock` is shared by:
10
+ * - SessionStart hook (kicks off the conversation with a snapshot)
11
+ * - UserPromptSubmit hook (fallback when search comes up empty)
12
+ *
13
+ * Centralising here so a tweak — wider truncation, extra fields, an
14
+ * empty-state message — lands once instead of drifting between two
15
+ * copies. The previous setup duplicated the whole function and we'd
16
+ * already accumulated minor divergences (truncation lengths matched
17
+ * but the function-name comments did not).
18
+ */
19
+
20
+ /**
21
+ * Render `profile` into a markdown block wrapped in <everme_profile>.
22
+ * Returns "" when there's nothing to show — callers skip injection in
23
+ * that case.
24
+ */
25
+ export function renderProfileBlock(profile) {
26
+ if (!profile) return "";
27
+ const explicit = Array.isArray(profile.explicit_info) ? profile.explicit_info : [];
28
+ const implicit = Array.isArray(profile.implicit_traits) ? profile.implicit_traits : [];
29
+ if (explicit.length === 0 && implicit.length === 0) return "";
30
+
31
+ const lines = ["<everme_profile>"];
32
+ if (explicit.length > 0) {
33
+ lines.push("Profile facts:");
34
+ for (const e of explicit.slice(0, 12)) {
35
+ const cat = e.category ? `[${e.category}] ` : "";
36
+ const desc = e.description || e.evidence || "";
37
+ if (!desc) continue;
38
+ lines.push(`- ${cat}${truncate(desc, 240)}`);
39
+ }
40
+ }
41
+ if (implicit.length > 0) {
42
+ lines.push("Implicit traits:");
43
+ for (const t of implicit.slice(0, 6)) {
44
+ const name = t.trait || t.name || "trait";
45
+ const desc = t.description || "";
46
+ lines.push(`- ${name}: ${truncate(desc, 200)}`);
47
+ }
48
+ }
49
+ lines.push("</everme_profile>");
50
+ return lines.join("\n");
51
+ }
52
+
53
+ /**
54
+ * Number of items the gateway included in this profile — used by the
55
+ * hook output's `systemMessage` ("loaded N items"). Counts both the
56
+ * explicit and implicit lists; matches what renderProfileBlock will
57
+ * surface (modulo per-list truncation, which is fine for a count).
58
+ */
59
+ export function profileItemCount(profile) {
60
+ if (!profile) return 0;
61
+ return (
62
+ (Array.isArray(profile.explicit_info) ? profile.explicit_info.length : 0) +
63
+ (Array.isArray(profile.implicit_traits) ? profile.implicit_traits.length : 0)
64
+ );
65
+ }
66
+
67
+ /**
68
+ * One-line truncation: collapse whitespace, cap to `n` chars, append
69
+ * ellipsis. Both block renderers use the same shape so the output is
70
+ * visually consistent across SessionStart and UserPromptSubmit.
71
+ */
72
+ export function truncate(s, n) {
73
+ s = String(s).replace(/\s+/g, " ").trim();
74
+ return s.length <= n ? s : s.slice(0, n - 1) + "…";
75
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Re-exports the SDK's redactError so hook scripts have a single
3
+ * import target. `debug` is plugin-local because it formats output
4
+ * with the [everme:<prefix>] tag specific to Claude Code stderr.
5
+ */
6
+
7
+ import { redactError } from "@everme/agent-sdk";
8
+
9
+ export { redactError };
10
+
11
+ const DEBUG = process.env.EVERME_DEBUG === "1";
12
+
13
+ /**
14
+ * Synchronous stderr trace, gated on EVERME_DEBUG=1. Earlier revisions
15
+ * dynamic-imported `redactError` here to "guard against tests mocking
16
+ * the SDK at module-load time" — but the static export above already
17
+ * load-fails in that scenario, AND the dynamic import is async, so
18
+ * any line written after `process.exit(0)` was silently dropped. Using
19
+ * the static `redactError` makes debug logging actually appear.
20
+ */
21
+ export function debug(prefix, ...args) {
22
+ if (!DEBUG) return;
23
+ try {
24
+ process.stderr.write(
25
+ `[everme:${prefix}] ` +
26
+ args
27
+ .map((a) => (typeof a === "string" ? a : safeStringify(a)))
28
+ .map(redactError)
29
+ .join(" ") +
30
+ "\n",
31
+ );
32
+ } catch {
33
+ /* never throw from debug */
34
+ }
35
+ }
36
+
37
+ function safeStringify(v) {
38
+ try {
39
+ return JSON.stringify(v);
40
+ } catch {
41
+ return String(v);
42
+ }
43
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Stable per-machine source key for documentKey derivation.
3
+ *
4
+ * The hooks pass this into `buildDocumentKey(sourceKey, logicalPath)`
5
+ * to anchor a version chain. In evt mode the agentId (`agt_…`) is
6
+ * unique per machine + user + platform, so it works as the source key
7
+ * unmodified. In emk mode the user has no agentId, so the previous
8
+ * code fell back to the literal `"agt_claude_code"` — which means
9
+ * every user on every machine sharing the same EverMe account would
10
+ * write into the SAME version chain, overwriting each other's runtime
11
+ * docs.
12
+ *
13
+ * This module computes a stable replacement: `claude-code:<host>:<user>`,
14
+ * SHA256-truncated to a 24-hex prefix. Same machine + user → same
15
+ * key across processes and reboots; two machines under one account →
16
+ * two distinct chains.
17
+ */
18
+
19
+ import { createHash } from "node:crypto";
20
+ import { hostname, userInfo } from "node:os";
21
+
22
+ let cached = null;
23
+
24
+ /**
25
+ * Returns the source key the runtime hooks should pass into
26
+ * buildDocumentKey. Honours `cfg.agentId` (evt mode) when present —
27
+ * agentId is already the canonical per-machine fingerprint that
28
+ * evercli writes — and falls back to a hashed host+user string when
29
+ * the user is on emk auth.
30
+ */
31
+ export function getSourceKey(cfg) {
32
+ if (cfg?.agentId) return cfg.agentId;
33
+ if (cached) return cached;
34
+ let host = "unknown-host";
35
+ let user = "unknown-user";
36
+ try {
37
+ host = hostname() || host;
38
+ } catch {
39
+ /* fall through */
40
+ }
41
+ try {
42
+ user = userInfo().username || user;
43
+ } catch {
44
+ /* fall through */
45
+ }
46
+ const sum = createHash("sha256").update(`claude-code:${host}:${user}`).digest("hex");
47
+ cached = "agt_emk_" + sum.slice(0, 24);
48
+ return cached;
49
+ }
50
+
51
+ /** Test seam — wipes the per-process cache. */
52
+ export function _reset() {
53
+ cached = null;
54
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Claude Code transcript reader.
3
+ *
4
+ * Stop / SessionEnd hooks receive `transcript_path` on stdin — a JSONL
5
+ * file Claude Code writes per session. Each line is one event; we
6
+ * parse selectively into the message shape EverMe wants:
7
+ *
8
+ * { role, text, ts, hasToolCall }
9
+ * { role, timestamp, content, toolCalls?, toolCallId? } for realtime writes
10
+ *
11
+ * `hasToolCall` is retained for legacy markdown rendering tests. Runtime
12
+ * persistence uses extractAgentMessages so tool calls/results can go directly
13
+ * to /mem/agent-memory.
14
+ */
15
+
16
+ import { existsSync } from "fs";
17
+ import { readFile } from "fs/promises";
18
+ import { AGENT_MEMORY_ROLES, AGENT_MEMORY_TOOL_CALL_TYPES } from "@everme/agent-sdk";
19
+
20
+ const READ_RETRIES = 5;
21
+ const RETRY_DELAY_MS = 100;
22
+
23
+ /**
24
+ * Read the transcript with a small retry budget. Stop hook can fire
25
+ * before Claude Code has flushed the final line; we wait for the
26
+ * `turn_duration` marker which Claude Code writes when the turn is
27
+ * complete.
28
+ *
29
+ * Async IO: hooks run in their own short-lived Node process, but
30
+ * transcripts can grow to several MB and the previous synchronous
31
+ * `readFileSync` blocked the event loop for the entire read on each
32
+ * of the 5 retries. Using fs.promises.readFile lets the loop service
33
+ * other tasks (timer for sleep, GC) between retries. The retry budget
34
+ * itself is unchanged.
35
+ */
36
+ export async function readTranscript(path) {
37
+ if (!path || !existsSync(path)) return [];
38
+ for (let i = 0; i < READ_RETRIES; i++) {
39
+ const raw = await readFile(path, "utf8");
40
+ const lines = raw.trim().split("\n").filter(Boolean);
41
+ if (lines.length === 0) {
42
+ await sleep(RETRY_DELAY_MS);
43
+ continue;
44
+ }
45
+ let complete = false;
46
+ try {
47
+ const last = JSON.parse(lines[lines.length - 1]);
48
+ if (last?.type === "turn_duration" || last?.event === "turn_duration") {
49
+ complete = true;
50
+ }
51
+ } catch {
52
+ /* incomplete line — retry */
53
+ }
54
+ if (complete || i === READ_RETRIES - 1) return lines;
55
+ await sleep(RETRY_DELAY_MS);
56
+ }
57
+ return [];
58
+ }
59
+
60
+ function sleep(ms) {
61
+ return new Promise((r) => setTimeout(r, ms));
62
+ }
63
+
64
+ /**
65
+ * Walk the JSONL events and produce the {role, text, hasToolCall, ts}
66
+ * sequence the hooks need. Robust to Claude Code's evolving transcript
67
+ * shape — we only consume the fields we recognise and silently skip
68
+ * unknown event kinds.
69
+ */
70
+ export function extractTurns(lines) {
71
+ const turns = [];
72
+ for (const line of lines) {
73
+ let ev;
74
+ try {
75
+ ev = JSON.parse(line);
76
+ } catch {
77
+ continue;
78
+ }
79
+ // User-side prompt
80
+ if (ev.role === AGENT_MEMORY_ROLES.USER && typeof ev.content === "string") {
81
+ turns.push({
82
+ role: AGENT_MEMORY_ROLES.USER,
83
+ text: ev.content,
84
+ ts: ev.timestamp || Date.now(),
85
+ hasToolCall: false,
86
+ });
87
+ continue;
88
+ }
89
+ // Assistant message — content can be a string or an array of
90
+ // content blocks (text / tool_use / tool_result).
91
+ if (ev.role === AGENT_MEMORY_ROLES.ASSISTANT) {
92
+ const { text, hasToolCall } = flattenAssistant(ev.content);
93
+ if (text) {
94
+ turns.push({
95
+ role: AGENT_MEMORY_ROLES.ASSISTANT,
96
+ text,
97
+ ts: ev.timestamp || Date.now(),
98
+ hasToolCall,
99
+ });
100
+ }
101
+ continue;
102
+ }
103
+ // Tool result event — Claude Code emits these as a separate role.
104
+ if (ev.role === AGENT_MEMORY_ROLES.TOOL || ev.type === "tool_result") {
105
+ const tr =
106
+ typeof ev.content === "string"
107
+ ? ev.content
108
+ : safeJsonStringify(ev.content);
109
+ turns.push({
110
+ role: AGENT_MEMORY_ROLES.TOOL,
111
+ text: tr,
112
+ ts: ev.timestamp || Date.now(),
113
+ hasToolCall: true,
114
+ });
115
+ }
116
+ }
117
+ return turns;
118
+ }
119
+
120
+ export function extractAgentMessages(lines) {
121
+ const messages = [];
122
+ for (const line of lines) {
123
+ let ev;
124
+ try {
125
+ ev = JSON.parse(line);
126
+ } catch {
127
+ continue;
128
+ }
129
+ const timestamp = normalizeTimestamp(ev.timestamp);
130
+ if (ev.role === AGENT_MEMORY_ROLES.USER) {
131
+ const content = textFromContent(ev.content);
132
+ if (content) messages.push({ role: AGENT_MEMORY_ROLES.USER, timestamp, content });
133
+ continue;
134
+ }
135
+ if (ev.role === AGENT_MEMORY_ROLES.ASSISTANT) {
136
+ const msg = agentAssistantMessage(ev.content, timestamp);
137
+ if (msg) messages.push(msg);
138
+ continue;
139
+ }
140
+ if (ev.role === AGENT_MEMORY_ROLES.TOOL || ev.type === "tool_result") {
141
+ const toolCallId = ev.toolCallId || ev.tool_call_id || ev.tool_use_id;
142
+ if (!toolCallId) continue;
143
+ const content =
144
+ typeof ev.content === "string" ? ev.content : safeJsonStringify(ev.content);
145
+ messages.push({ role: AGENT_MEMORY_ROLES.TOOL, timestamp, toolCallId, content });
146
+ }
147
+ }
148
+ return messages;
149
+ }
150
+
151
+ function agentAssistantMessage(content, timestamp) {
152
+ if (typeof content === "string") {
153
+ return content ? { role: AGENT_MEMORY_ROLES.ASSISTANT, timestamp, content } : null;
154
+ }
155
+ if (!Array.isArray(content)) return null;
156
+ const textParts = [];
157
+ const toolCalls = [];
158
+ for (const [i, b] of content.entries()) {
159
+ if (!b || typeof b !== "object") continue;
160
+ if (b.type === "text" && typeof b.text === "string") {
161
+ textParts.push(b.text);
162
+ } else if (b.type === "tool_use" || b.type === "toolCall") {
163
+ const args = b.input ?? b.arguments ?? {};
164
+ toolCalls.push({
165
+ id: b.id || b.tool_use_id || `claude_tool_${timestamp}_${i}`,
166
+ type: AGENT_MEMORY_TOOL_CALL_TYPES.FUNCTION,
167
+ name: b.name || "unknown",
168
+ arguments: typeof args === "string" ? args : safeJsonStringify(args),
169
+ });
170
+ }
171
+ }
172
+ const out = { role: AGENT_MEMORY_ROLES.ASSISTANT, timestamp };
173
+ const text = textParts.join("\n\n");
174
+ if (text) out.content = text;
175
+ if (toolCalls.length) out.toolCalls = toolCalls;
176
+ return out.content || out.toolCalls ? out : null;
177
+ }
178
+
179
+ function textFromContent(content) {
180
+ if (typeof content === "string") return content;
181
+ if (!Array.isArray(content)) return "";
182
+ return content
183
+ .map((b) => {
184
+ if (typeof b === "string") return b;
185
+ if (b?.type === "text" && typeof b.text === "string") return b.text;
186
+ return "";
187
+ })
188
+ .filter(Boolean)
189
+ .join("\n");
190
+ }
191
+
192
+ function normalizeTimestamp(ts) {
193
+ if (typeof ts === "number" && Number.isFinite(ts)) {
194
+ return ts > 10_000_000_000 ? Math.trunc(ts) : Math.trunc(ts * 1000);
195
+ }
196
+ const parsed = Date.parse(ts);
197
+ if (Number.isFinite(parsed)) return parsed;
198
+ return Date.now();
199
+ }
200
+
201
+ function flattenAssistant(content) {
202
+ if (typeof content === "string") {
203
+ return { text: content, hasToolCall: false };
204
+ }
205
+ if (!Array.isArray(content)) return { text: "", hasToolCall: false };
206
+ const parts = [];
207
+ let hasToolCall = false;
208
+ for (const b of content) {
209
+ if (!b || typeof b !== "object") continue;
210
+ if (b.type === "text" && typeof b.text === "string") {
211
+ parts.push(b.text);
212
+ } else if (b.type === "tool_use") {
213
+ hasToolCall = true;
214
+ parts.push(
215
+ `[tool_use ${b.name || "unknown"}] ${safeJsonStringify(b.input)}`,
216
+ );
217
+ } else if (b.type === "tool_result") {
218
+ hasToolCall = true;
219
+ const tr =
220
+ typeof b.content === "string" ? b.content : safeJsonStringify(b.content);
221
+ parts.push(`[tool_result ${b.tool_use_id || ""}] ${tr}`);
222
+ }
223
+ }
224
+ return { text: parts.join("\n\n"), hasToolCall };
225
+ }
226
+
227
+ function safeJsonStringify(v) {
228
+ try {
229
+ return JSON.stringify(v);
230
+ } catch {
231
+ return String(v);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Build a single markdown document from a turn sequence — same shape
237
+ * the @everme/memory-mcp runtime buffer writes, so backend chunkers /
238
+ * classifiers behave identically.
239
+ */
240
+ export function buildTranscriptMarkdown(turns, { sessionId, agentId }) {
241
+ const now = new Date().toISOString();
242
+ const lines = [
243
+ "---",
244
+ `everme_runtime_version: 1`,
245
+ `agent_id: ${agentId || "agt_claude_code"}`,
246
+ `session_key: ${sessionId || "claude-code-session"}`,
247
+ `last_flushed_at: ${now}`,
248
+ `turn_count: ${turns.length}`,
249
+ "---",
250
+ "",
251
+ `# Claude Code session ${sessionId || ""}`,
252
+ "",
253
+ ];
254
+ for (const t of turns) {
255
+ lines.push(`## ${t.role} · ${new Date(t.ts).toISOString()}`);
256
+ lines.push("");
257
+ lines.push(t.text);
258
+ lines.push("");
259
+ lines.push("---");
260
+ lines.push("");
261
+ }
262
+ return lines.join("\n");
263
+ }
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server bundled with the Claude Code plugin. Exposes the
4
+ * EverMe gateway's search + context endpoints as MCP tools so users
5
+ * can ALSO recall memory manually via natural language ("search my
6
+ * memory for the Postgres index thing"), even when the
7
+ * UserPromptSubmit hook has already done implicit recall.
8
+ *
9
+ * Wire format: MCP stdio transport (JSON-RPC 2.0 framed by line).
10
+ * We hand-roll the tiny subset Claude Code uses rather than pulling
11
+ * in @modelcontextprotocol/sdk — keeps the install fast (no npm
12
+ * install required) and the dependency surface minimal.
13
+ *
14
+ * Tools:
15
+ * everme_search — POST /api/v1/mem/search
16
+ * everme_context — POST /api/v1/mem/context (server-rendered prompt block)
17
+ */
18
+
19
+ import { createInterface } from "readline";
20
+ import { searchMemories, getContext, EvermeError } from "./lib/api.js";
21
+ import { isConfigured } from "./lib/config.js";
22
+ import { redactError, debug } from "./lib/redact.js";
23
+
24
+ const TOOLS = [
25
+ {
26
+ name: "everme_search",
27
+ description:
28
+ "Search EverMe memories from past sessions. Returns ranked memory items with subject, summary, and relevance score. Use when the user asks about previous work, decisions, or context. Params: query (required), topK (default 10, max 25).",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ query: { type: "string", description: "Search query — keywords or a question" },
33
+ topK: { type: "number", description: "Max results (default 10, max 25)" },
34
+ },
35
+ required: ["query"],
36
+ },
37
+ },
38
+ {
39
+ name: "everme_context",
40
+ description:
41
+ "Fetch the server-rendered context block (profile + recent episodes) the gateway uses for prompt injection. Useful when you want a single ready-to-paste summary. Params: query (optional), topK (default 5).",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ query: { type: "string", description: "Optional query for relevance-biased context" },
46
+ topK: { type: "number", description: "Max items to include (default 5)" },
47
+ },
48
+ },
49
+ },
50
+ ];
51
+
52
+ const handlers = {
53
+ initialize: () => ({
54
+ protocolVersion: "2024-11-05",
55
+ capabilities: { tools: { listChanged: false } },
56
+ serverInfo: { name: "everme", version: "0.1.0" },
57
+ }),
58
+ "tools/list": () => ({ tools: TOOLS }),
59
+ "tools/call": async (params) => {
60
+ const name = params?.name;
61
+ const args = params?.arguments || {};
62
+ if (!isConfigured()) {
63
+ return errResp("EverMe not configured: set EVERME_API_KEY (emk) or EVERME_AGENT_TOKEN");
64
+ }
65
+ try {
66
+ switch (name) {
67
+ case "everme_search": {
68
+ const topK = Math.min(Number(args.topK) || 10, 25);
69
+ const res = await searchMemories(String(args.query || ""), { topK });
70
+ return ok(JSON.stringify(res, null, 2));
71
+ }
72
+ case "everme_context": {
73
+ const topK = Number(args.topK) || 5;
74
+ const res = await getContext(String(args.query || ""), { topK });
75
+ return ok(JSON.stringify(res, null, 2));
76
+ }
77
+ default:
78
+ return errResp(`unknown tool: ${name}`);
79
+ }
80
+ } catch (err) {
81
+ const safe = redactError(err instanceof EvermeError ? err.message : err?.message || String(err));
82
+ debug("mcp", `tool ${name} failed:`, safe);
83
+ return errResp(safe);
84
+ }
85
+ },
86
+ };
87
+
88
+ function ok(text) {
89
+ return { content: [{ type: "text", text: String(text ?? "") }] };
90
+ }
91
+ function errResp(msg) {
92
+ return { isError: true, content: [{ type: "text", text: `error: ${msg}` }] };
93
+ }
94
+
95
+ const rl = createInterface({ input: process.stdin, terminal: false });
96
+ rl.on("line", async (line) => {
97
+ let req;
98
+ try {
99
+ req = JSON.parse(line);
100
+ } catch {
101
+ return; // ignore malformed lines
102
+ }
103
+ const handler = handlers[req?.method];
104
+ if (!handler) {
105
+ if (req?.id != null) {
106
+ respond(req.id, undefined, { code: -32601, message: `method not found: ${req?.method}` });
107
+ }
108
+ return;
109
+ }
110
+ try {
111
+ const result = await handler(req?.params);
112
+ if (req?.id != null) respond(req.id, result);
113
+ } catch (err) {
114
+ if (req?.id != null) {
115
+ respond(req.id, undefined, {
116
+ code: -32000,
117
+ message: redactError(err?.message || String(err)),
118
+ });
119
+ }
120
+ }
121
+ });
122
+
123
+ function respond(id, result, error) {
124
+ const env = error
125
+ ? { jsonrpc: "2.0", id, error }
126
+ : { jsonrpc: "2.0", id, result };
127
+ process.stdout.write(JSON.stringify(env) + "\n");
128
+ }
129
+
130
+ debug("mcp", "everme MCP server ready");