@everme/claude-code 0.4.1 → 0.5.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.
@@ -10,7 +10,7 @@
10
10
  "name": "everme",
11
11
  "source": "./",
12
12
  "description": "Automatic memory recall for Claude Code through the EverMe gateway. Saves and recalls per-session context using your EverMe account credentials.",
13
- "version": "0.4.1",
13
+ "version": "0.5.0",
14
14
  "homepage": "https://everme.evermind.ai",
15
15
  "license": "Apache-2.0"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "everme",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "EverMe — automatic memory recall for Claude Code. Recalls relevant context from past sessions before each prompt and saves new turns through the EverMe gateway.",
5
5
  "author": {
6
6
  "name": "EverMind AI",
package/README.md CHANGED
@@ -11,7 +11,7 @@ Automatic memory recall + persistence for Claude Code, backed by the EverMe gate
11
11
 
12
12
  Plus:
13
13
 
14
- - **MCP server** exposing `everme_search` + `everme_context` tools for explicit recall.
14
+ - **MCP server** exposing the canonical `mem_search` / `mem_context` / `mem_save_fact` / `mem_save_turn` tools for explicit recall and saves.
15
15
  - **Slash commands** `/recall <q>` and `/everme-help`.
16
16
  - **Skill** (`memory-tools`) that tells Claude when/how to use the search tool.
17
17
 
@@ -76,7 +76,7 @@ In a new Claude Code session:
76
76
  /recall the postgres composite index decision
77
77
  ```
78
78
 
79
- …should call `everme_search`, summarise hits, and cite memory subjects.
79
+ …should call `mem_search`, summarise hits, and cite memory subjects.
80
80
 
81
81
  ## Files
82
82
 
@@ -89,7 +89,7 @@ hooks/scripts/inject-memories.js UserPromptSubmit handler
89
89
  hooks/scripts/store-memories.js Stop handler
90
90
  hooks/scripts/session-start.js SessionStart handler
91
91
  hooks/scripts/session-summary.js SessionEnd handler
92
- hooks/scripts/mcp-server.js MCP server (everme_search / everme_context tools)
92
+ hooks/scripts/mcp-server.js MCP server (canonical mem_* tools)
93
93
  hooks/scripts/lib/adapter.js Claude Code stdin/transcript/stdout adapter
94
94
  hooks/scripts/lib/run-hook.js thin shared-runtime entry helper
95
95
  hooks/scripts/lib/config.js Env-var resolution (emk vs evt)
@@ -14,7 +14,7 @@ Auth: EVERME_API_KEY (account emk_*) — recall-only mode
14
14
  Gateway: EVERME_API_BASE — defaults to https://api.everme.evermind.ai
15
15
 
16
16
  Hooks:
17
- SessionStart → loads recent context from past sessions
17
+ SessionStart → loads the durable Profile snapshot
18
18
  UserPromptSubmit → recalls relevant memories before each prompt
19
19
  Stop → saves the last raw turn through /mem/agent-memory
20
20
  SessionEnd → no persistence; Stop owns runtime writes
@@ -24,6 +24,8 @@ Slash:
24
24
  /everme-help — this card
25
25
 
26
26
  MCP tools:
27
- everme_search ranked search
28
- everme_context server-rendered context block
27
+ mem_search hybrid search across all memory buckets
28
+ mem_context durable Profile snapshot only
29
+ mem_save_fact — save a durable user fact to the Profile path
30
+ mem_save_turn — save a reusable task trajectory
29
31
  ```
@@ -8,10 +8,10 @@ arguments:
8
8
 
9
9
  # EverMe · recall
10
10
 
11
- You have access to two MCP tools backed by the EverMe gateway:
11
+ Use the canonical EverMe MCP tools:
12
12
 
13
- - `everme_search` — ranked search with subject + summary + score.
14
- - `everme_context` — server-rendered context block (profile + recent episodes).
13
+ - `mem_search` — hybrid search across episodic memories, profile entries, agent cases/skills, and the recent raw transcript.
14
+ - `mem_context` — durable Profile snapshot only; it does not search past conversations and is not needed for this command.
15
15
 
16
16
  ## Query
17
17
 
@@ -19,8 +19,9 @@ You have access to two MCP tools backed by the EverMe gateway:
19
19
 
20
20
  ## Instructions
21
21
 
22
- 1. Call `everme_search` with the query. Start with `topK: 10`.
23
- 2. If the top results are clearly relevant (score ≥ 0.3), summarize the matched memories briefly and use them to answer or guide the next action.
22
+ 1. Call `mem_search` with a short version of the query. Start with `topK: 10`.
23
+ 2. If the returned memories are clearly relevant, summarize them briefly and use them to answer or guide the next action.
24
24
  3. If results are weak or empty, retry once with broader keywords. If still nothing useful, say so explicitly — do not fabricate context.
25
- 4. When citing a memory, mention its subject (or session id) so the user can trace it back via `evercli` or the EverMe Web UI.
26
- 5. NEVER paste the entire raw memory body verbatim if it's long; quote the salient parts only.
25
+ 4. Treat rows under "Recent unextracted transcript" as provisional, not as established facts or confirmed decisions.
26
+ 5. When citing a memory, mention its subject or session id when available so the user can trace it through `evercli` or the EverMe Web UI.
27
+ 6. NEVER paste an entire long memory body verbatim; quote only the salient parts.
@@ -1,19 +1,17 @@
1
1
  #!/usr/bin/env node
2
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.
3
+ * MCP server bundled with the Claude Code plugin. Exposes the canonical
4
+ * EverMe four-tool catalogue (mem_search / mem_context / mem_save_turn /
5
+ * mem_save_fact the same public ABI as @everme/memory-mcp and the Go
6
+ * hosted /mcp surface).
8
7
  *
9
8
  * Wire format: MCP stdio transport (JSON-RPC 2.0 framed by line).
10
9
  * We hand-roll the tiny subset Claude Code uses rather than pulling
11
10
  * 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)
11
+ * install required) and the dependency surface minimal. The canonical
12
+ * tools are a thin adapter over @everme/agent-sdk helpers — this
13
+ * package must NOT import @everme/memory-mcp (host packages depend on
14
+ * agent-sdk only).
17
15
  */
18
16
 
19
17
  import { createInterface } from "readline";
@@ -21,9 +19,13 @@ import { createRequire } from "node:module";
21
19
  import {
22
20
  buildMemoryPrompt,
23
21
  createClient,
22
+ getContext,
24
23
  searchMemory,
25
- renderProfileBlock,
24
+ saveAgentMemory,
25
+ savePersonalMemory,
26
+ AGENT_MEMORY_ROLES,
26
27
  redactError,
28
+ describeError,
27
29
  EvermeError,
28
30
  } from "@everme/agent-sdk";
29
31
  import { getConfig, isConfigured } from "./lib/config.js";
@@ -47,34 +49,249 @@ const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2024-11-05", "2025-03-26"]);
47
49
  const LATEST_PROTOCOL_VERSION = "2025-03-26";
48
50
  let client;
49
51
 
52
+ // stdout carries the JSON-RPC stream; HTTP diagnostics (per-request
53
+ // requestId lines) go to stderr like every other hook surface.
54
+ const stderrLog = {
55
+ info(line) {
56
+ try {
57
+ process.stderr.write(`${line}\n`);
58
+ } catch {
59
+ // A closed stderr must never break the MCP stream.
60
+ }
61
+ },
62
+ warn(line) {
63
+ this.info(line);
64
+ },
65
+ };
66
+
50
67
  function getClient() {
51
- if (!client) client = createClient(getConfig());
68
+ if (!client) client = createClient(getConfig(), stderrLog);
52
69
  return client;
53
70
  }
54
71
 
72
+ // Instructions returned on initialize — Claude Code splices them into the
73
+ // system prompt. Mirrors @everme/memory-mcp's EVERME_MCP_INSTRUCTIONS with
74
+ // the Claude-Code-specific note that native hooks already inject
75
+ // <everme_profile> / <everme_recall> and save turns automatically.
76
+ const INSTRUCTIONS = [
77
+ "EverMe memory is connected. This plugin's native hooks already inject a",
78
+ "<everme_profile> block at session start and a <everme_recall> block before",
79
+ "each prompt, and they save the conversation automatically — so the tools",
80
+ "below are for the cases the hooks do not cover. Call them AUTONOMOUSLY when",
81
+ "a trigger fires; never wait for the user to say \"remember\" or \"recall\".",
82
+ "1. The <everme_recall> block is missing, empty, or clearly irrelevant AND the user references earlier conversations, decisions, conventions, or previously solved problems — call `mem_search` with a SHORT query. Do not repeat an identical query in the same turn.",
83
+ "2. The user states a durable fact about themselves (a preference, habit, trait, long-term goal, or decision) — call `mem_save_fact` immediately. Only `extracted:true` / `profileUpdated:true` means the profile really updated; on `no_extraction` do NOT tell the user the fact was remembered, and do not auto-retry.",
84
+ "3. A task was solved in a way worth reusing and the trajectory is NOT already captured by the hooks — call `mem_save_turn` with the COMPLETE trajectory. It feeds episodic / case / skill extraction; chat-dual-write backends may also update the Profile, reported by profileUpdated.",
85
+ "4. No <everme_profile> block was injected this session — call `mem_context` once. It returns the durable Profile ONLY (no search, no episodes).",
86
+ ].join("\n");
87
+
55
88
  const TOOLS = [
56
89
  {
57
- name: "everme_search",
90
+ name: "mem_search",
58
91
  description:
59
- "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).",
92
+ "Search EverMe memory for entries relevant to a free-text query. " +
93
+ "Returns the top-K matching entries (episodic, profile, agent " +
94
+ "cases/skills, recent raw transcript) rendered as markdown; rows " +
95
+ "under the provisional transcript header are not yet extracted and " +
96
+ "must not be quoted as established facts.\n\n" +
97
+ "Call this proactively — without being asked — whenever the user " +
98
+ "references prior conversations, earlier decisions, project " +
99
+ "conventions, or previously solved problems (\"what did we say " +
100
+ "about X\", \"remember when…\", \"like last time\", \"did we fix " +
101
+ "this before\", \"continue where we left off\").\n\n" +
102
+ "Skip the call when the host already injected a non-empty, relevant " +
103
+ "<everme_recall> block this turn, and do not repeat an identical " +
104
+ "query within the same turn.\n\n" +
105
+ "`query` is KEYWORDS ONLY — the topic, not the conversation. Two to " +
106
+ "eight words, under ~100 characters, no sentences copied from the " +
107
+ "transcript. Never pass the user's whole message, a file, a log, a " +
108
+ "diff, or your own reasoning: the search embeds whatever you send, " +
109
+ "so boilerplate crowds out the topic and the results get worse. " +
110
+ "Good: \"oauth token rotation\". Bad: the last three turns pasted " +
111
+ "in. Rely on the default topK of 10; only raise it if a first " +
112
+ "search genuinely missed.",
60
113
  inputSchema: {
61
114
  type: "object",
62
115
  properties: {
63
- query: { type: "string", description: "Search query — keywords or a question" },
64
- topK: { type: "number", description: "Max results (default 10, max 25)" },
116
+ query: {
117
+ type: "string",
118
+ description:
119
+ "Keywords naming the topic to recall — two to eight words, " +
120
+ "under ~100 characters. Not a sentence from the transcript, " +
121
+ "not the user's whole message, not a pasted file or log.",
122
+ },
123
+ topK: { type: "integer", description: "Max entries to return", default: 10 },
65
124
  },
66
125
  required: ["query"],
67
126
  },
68
127
  },
69
128
  {
70
- name: "everme_context",
129
+ name: "mem_context",
130
+ description:
131
+ "Read the current user's durable Profile snapshot ONLY. This tool " +
132
+ "never performs semantic search and never returns episodic memories, " +
133
+ "raw messages, agent cases, or agent skills.\n\n" +
134
+ "Call it ONCE at the start of a session, and only when no " +
135
+ "<everme_profile> block was injected. Do NOT use it as a fallback for " +
136
+ "recalling past decisions, old sessions, or task context — that is " +
137
+ "mem_search's job.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ query: {
142
+ type: "string",
143
+ description:
144
+ "Deprecated and ignored — mem_context never performs semantic " +
145
+ "search. Kept for backwards compatibility only.",
146
+ },
147
+ forceRefresh: {
148
+ type: "boolean",
149
+ default: false,
150
+ description: "Bypass the server-side profile cache and re-read the upstream profile.",
151
+ },
152
+ },
153
+ },
154
+ },
155
+ {
156
+ name: "mem_save_turn",
71
157
  description:
72
- "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 10).",
158
+ "Persist a conversation trajectory in realtime via /mem/agent-memory. " +
159
+ "Use sessionKey as conversationId.\n\n" +
160
+ "Call this when a task was solved in a way worth reusing AND the " +
161
+ "trajectory is not already captured by the plugin's automatic " +
162
+ "transcript save. Pass the COMPLETE round-trip — messages: [{role, " +
163
+ "content, timestamp?, toolCalls?, toolCallId?}] — EverOS only " +
164
+ "extracts agent_case / agent_skill from trajectories carrying the " +
165
+ "full tool round-trip.\n\n" +
166
+ "By default flush=true: extraction into episodic / agent_case / " +
167
+ "agent_skill runs right away. Pass flush=false for append-only " +
168
+ "accumulation. The primary trajectory path extracts episodic / case / " +
169
+ "skill memory; chat-dual-write backends may also update the user's " +
170
+ "Profile. Check profileUpdated for the derived profile verdict, and " +
171
+ "use mem_save_fact for deliberate durable user facts.",
73
172
  inputSchema: {
74
173
  type: "object",
75
174
  properties: {
76
- query: { type: "string", description: "Optional query for relevance-biased context" },
77
- topK: { type: "number", description: "Max items to include (default 10)" },
175
+ role: {
176
+ type: "string",
177
+ enum: ["user", "assistant", "tool"],
178
+ description: "Role for the single-message form. Ignored when messages[] is set.",
179
+ },
180
+ text: { type: "string", description: "Content for the single-message form. Ignored when messages[] is set." },
181
+ timestamp: { type: "integer", description: "Unix milliseconds; defaults to now. Single-message form only." },
182
+ toolCallId: { type: "string", description: "Required when role=tool. Single-message form only." },
183
+ toolCalls: {
184
+ type: "array",
185
+ description:
186
+ "Tool invocations made by an assistant message. Required when " +
187
+ "the assistant called tools — without this the tool round-trip " +
188
+ "can never be reconstructed downstream and EverOS will not " +
189
+ "produce agent_case / agent_skill from the turn.",
190
+ items: {
191
+ type: "object",
192
+ properties: {
193
+ id: { type: "string" },
194
+ name: { type: "string" },
195
+ arguments: { type: "string", description: "JSON-encoded tool arguments (stringified, not an object)." },
196
+ },
197
+ required: ["id", "name", "arguments"],
198
+ },
199
+ },
200
+ messages: {
201
+ type: "array",
202
+ description:
203
+ "Multi-message trajectory. Preferred for recording a complete " +
204
+ "user → assistant{tool_use} → tool{tool_result} → assistant cycle.",
205
+ items: {
206
+ type: "object",
207
+ properties: {
208
+ role: { type: "string", enum: ["user", "assistant", "tool"] },
209
+ content: { description: "String text, or array of content items (text / image / doc)." },
210
+ timestamp: { type: "integer" },
211
+ toolCallId: { type: "string", description: "Required when role=tool." },
212
+ toolCalls: {
213
+ type: "array",
214
+ items: {
215
+ type: "object",
216
+ properties: {
217
+ id: { type: "string" },
218
+ name: { type: "string" },
219
+ arguments: { type: "string" },
220
+ },
221
+ required: ["id", "name", "arguments"],
222
+ },
223
+ },
224
+ },
225
+ required: ["role"],
226
+ },
227
+ },
228
+ sessionKey: { type: "string", description: "Session id; defaults to 'default'." },
229
+ flush: {
230
+ type: "boolean",
231
+ default: true,
232
+ description:
233
+ "true (default) = trigger EverOS extraction into " +
234
+ "episodic_memory / agent_case / agent_skill after writing; " +
235
+ "false = append-only, messages are searchable as raw_messages " +
236
+ "only and extraction is deferred until a later flush. The " +
237
+ "default flipped from false to true because callers rarely " +
238
+ "issued an explicit follow-up flush, leaving trajectories " +
239
+ "permanently stuck as raw_messages with zero case/skill.",
240
+ },
241
+ },
242
+ },
243
+ },
244
+ {
245
+ name: "mem_save_fact",
246
+ description:
247
+ "Persist a durable fact about the USER (a preference, habit, trait, " +
248
+ "or decision) via the long-term PROFILE write path — the block loaded " +
249
+ "at the start of every session.\n\n" +
250
+ "Call this proactively, without being asked, the moment the user " +
251
+ "states something true about themselves that should outlive this " +
252
+ "conversation (\"I love summer\", \"sign my docs as Alice\").\n\n" +
253
+ "This is the direct profile-producing sibling of mem_save_turn — " +
254
+ "mem_save_turn primarily records trajectories, though chat-dual-write " +
255
+ "backends may derive a profile update. With flush=true (default) " +
256
+ "the call runs the synchronous materialise path and returns the real " +
257
+ "EverOS verdict: only `extracted:true` / `profileUpdated:true` " +
258
+ "confirms the fact reached the profile. `status:\"no_extraction\"` " +
259
+ "means the profile did NOT update — never claim the fact was " +
260
+ "remembered in that case, and do not auto-retry.",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ fact: {
265
+ type: "string",
266
+ description:
267
+ "A single user-stated fact, recorded as one user-role message. " +
268
+ "Ignored when messages[] is set.",
269
+ },
270
+ messages: {
271
+ type: "array",
272
+ description:
273
+ "Explicit user/assistant turns. Tool roles are not accepted on " +
274
+ "this path. Preferred when you want to capture both the user's " +
275
+ "statement and your acknowledgement.",
276
+ items: {
277
+ type: "object",
278
+ properties: {
279
+ role: { type: "string", enum: ["user", "assistant"] },
280
+ content: { description: "String text, or array of content items." },
281
+ timestamp: { type: "integer", description: "Unix milliseconds; defaults to now." },
282
+ },
283
+ required: ["role"],
284
+ },
285
+ },
286
+ sessionKey: { type: "string", description: "Session id; defaults to 'default'." },
287
+ flush: {
288
+ type: "boolean",
289
+ default: true,
290
+ description:
291
+ "true (default) = issue EverOS flush and return its verdict; " +
292
+ "false = skip extraction entirely (fact accepted but not in the " +
293
+ "profile block).",
294
+ },
78
295
  },
79
296
  },
80
297
  },
@@ -90,6 +307,7 @@ const handlers = {
90
307
  protocolVersion,
91
308
  capabilities: { tools: { listChanged: false } },
92
309
  serverInfo: { name: "everme", version: PKG_VERSION },
310
+ instructions: INSTRUCTIONS,
93
311
  };
94
312
  },
95
313
  "tools/list": () => ({ tools: TOOLS }),
@@ -101,45 +319,105 @@ const handlers = {
101
319
  }
102
320
  try {
103
321
  switch (name) {
104
- case "everme_search": {
105
- // Render the SDK bundle through buildMemoryPrompt — same
106
- // path inject-memories.js uses for the auto-recall hook,
107
- // and the same shape @everme/memory-mcp's mem_search tool
108
- // returns. Earlier `JSON.stringify(res, null, 2)` forced the
109
- // host LLM to peel a JSON envelope and decode escaped
110
- // newlines before any of the section bullets were readable.
111
- const topK = Math.min(Number(args.topK) || 10, 25);
112
- const res = await searchMemory(getClient(), { query: String(args.query || ""), topK });
322
+ case "mem_search": {
323
+ const topK = Math.min(Number(args.topK) || 10, 50);
324
+ const res = await searchMemory(getClient(), { query: String(args.query || ""), topK }, stderrLog);
113
325
  const body = buildMemoryPrompt(res, { wrapInCodeBlock: false });
114
326
  const header = `## EverMe search results for "${String(args.query || "")}"`;
115
327
  const trimmed = body.replace(/^## Relevant memory\n\n?/, "");
116
328
  const text = trimmed
117
329
  ? `${header}\n\n${trimmed}`
118
330
  : `${header}\n\n_(no matching memories)_`;
119
- return ok(redactError(text));
331
+ return ok(appendRequestID(redactError(text), res?.requestId));
120
332
  }
121
- case "everme_context": {
122
- // getContext returns the gateway's raw shape
123
- // {profile, cachedAt, generatedAt} — the markdown lives in
124
- // res.profile as a structured object, NOT a string. Render
125
- // via renderProfileBlock (same renderer session-start.js
126
- // uses for the SessionStart hook injection) so the Tools
127
- // path matches what users already see in the injected
128
- // <everme_profile> block.
129
- const res = await getClient().request("POST", "/mem/context", {});
130
- // renderProfileBlock returns "" when profile exists but has no
131
- // facts/traits yet (new account). Check the rendered output, not
132
- // just the wrapper object, so the empty case yields a fallback
133
- // message instead of an empty tool result.
134
- const rendered = res?.profile ? renderProfileBlock(res.profile) : "";
135
- const text = rendered || "_(no profile available — your EverMe account has no extracted memories yet)_";
136
- return ok(redactError(text));
333
+ case "mem_context": {
334
+ // Profile-only: `query` is accepted for compat but ignored.
335
+ const ctx = await getContext(
336
+ getClient(),
337
+ "",
338
+ { forceRefresh: args.forceRefresh === true },
339
+ stderrLog,
340
+ );
341
+ const text = ctx?.context || "_(no profile available — your EverMe account has no extracted memories yet)_";
342
+ return ok(appendRequestID(redactError(text), ctx?.requestId));
343
+ }
344
+ case "mem_save_turn": {
345
+ let messages;
346
+ if (Array.isArray(args.messages) && args.messages.length) {
347
+ messages = args.messages.map(normaliseTurnMessage);
348
+ } else {
349
+ messages = [normaliseTurnMessage({
350
+ role: args.role || AGENT_MEMORY_ROLES.USER,
351
+ content: args.text,
352
+ timestamp: args.timestamp,
353
+ toolCallId: args.toolCallId,
354
+ toolCalls: args.toolCalls,
355
+ })];
356
+ }
357
+ const res = await saveAgentMemory(getClient(), {
358
+ conversationId: args.sessionKey || "default",
359
+ messages,
360
+ flush: args.flush !== false,
361
+ }, stderrLog);
362
+ return okJson({
363
+ saved: !!res,
364
+ accepted: !!res,
365
+ status: res?.status || null,
366
+ messageCount: res?.messageCount || 0,
367
+ flushed: !!res?.flushed,
368
+ profileStatus: res?.personalStatus || null,
369
+ profileUpdated: !!res?.personalExtracted,
370
+ requestId: res?.requestId || null,
371
+ });
372
+ }
373
+ case "mem_save_fact": {
374
+ let messages;
375
+ if (Array.isArray(args.messages) && args.messages.length) {
376
+ const bad = args.messages.find(
377
+ (m) => m?.role !== AGENT_MEMORY_ROLES.USER && m?.role !== AGENT_MEMORY_ROLES.ASSISTANT,
378
+ );
379
+ if (bad) {
380
+ return errResp(
381
+ `mem_save_fact accepts only 'user' or 'assistant' roles; got ${JSON.stringify(bad?.role)}. ` +
382
+ "The personal-memory path does not record tool turns — use mem_save_turn for trajectories.",
383
+ );
384
+ }
385
+ messages = args.messages.map((m) => ({
386
+ role: m?.role,
387
+ content: m?.content !== undefined ? m.content : m?.text,
388
+ timestamp: Number(m?.timestamp) || Date.now(),
389
+ }));
390
+ } else if (typeof args.fact === "string" && args.fact.trim()) {
391
+ messages = [{ role: AGENT_MEMORY_ROLES.USER, content: args.fact, timestamp: Date.now() }];
392
+ } else {
393
+ return errResp("mem_save_fact requires either `fact` or a non-empty `messages` array");
394
+ }
395
+ const res = await savePersonalMemory(getClient(), {
396
+ conversationId: args.sessionKey || "default",
397
+ messages,
398
+ flush: args.flush !== false,
399
+ }, stderrLog);
400
+ if (!res) {
401
+ return errResp("mem_save_fact wrote nothing — every message had empty content after normalization");
402
+ }
403
+ return okJson({
404
+ saved: true,
405
+ accepted: true,
406
+ status: res?.status || null,
407
+ messageCount: res?.messageCount || 0,
408
+ flushed: !!res?.flushed,
409
+ extracted: !!res?.extracted,
410
+ // profileUpdated aliases extracted — the only signal that the
411
+ // fact really materialised into the profile.
412
+ profileUpdated: !!res?.extracted,
413
+ requestId: res?.requestId || null,
414
+ });
137
415
  }
138
416
  default:
139
417
  return errResp(`unknown tool: ${name}`);
140
418
  }
141
419
  } catch (err) {
142
- const safe = redactError(err instanceof EvermeError ? err.message : err?.message || String(err));
420
+ const safe = describeError(err);
143
421
  return errResp(safe);
144
422
  }
145
423
  },
@@ -148,10 +426,37 @@ const handlers = {
148
426
  function ok(text) {
149
427
  return { content: [{ type: "text", text: String(text ?? "") }] };
150
428
  }
429
+ function okJson(data) {
430
+ return { content: [{ type: "text", text: JSON.stringify(data ?? {}, null, 2) }] };
431
+ }
151
432
  function errResp(msg) {
152
433
  return { isError: true, content: [{ type: "text", text: `error: ${msg}` }] };
153
434
  }
154
435
 
436
+ // appendRequestID mirrors @everme/memory-mcp: tack the trace id onto a
437
+ // markdown payload so a user can quote it to support.
438
+ function appendRequestID(text, requestId) {
439
+ if (!requestId) return text;
440
+ return `${text}\n\n_(requestId: ${requestId})_`;
441
+ }
442
+
443
+ // normaliseTurnMessage coerces an LLM-provided message into the SDK
444
+ // agent-memory shape — accepts both legacy {role, text} and canonical
445
+ // {role, content, toolCalls, toolCallId} forms. Mirrors the equivalent
446
+ // helper in @everme/memory-mcp (kept local: host packages must not
447
+ // import each other).
448
+ function normaliseTurnMessage(m) {
449
+ const role = m?.role || AGENT_MEMORY_ROLES.USER;
450
+ const out = { role, timestamp: Number(m?.timestamp) || Date.now() };
451
+ if (m?.content !== undefined) out.content = m.content;
452
+ else if (m?.text !== undefined) out.content = String(m.text);
453
+ if (Array.isArray(m?.toolCalls) && m.toolCalls.length) out.toolCalls = m.toolCalls;
454
+ if (role === AGENT_MEMORY_ROLES.TOOL && (m?.toolCallId || m?.tool_call_id)) {
455
+ out.toolCallId = String(m.toolCallId || m.tool_call_id);
456
+ }
457
+ return out;
458
+ }
459
+
155
460
  const rl = createInterface({ input: process.stdin, terminal: false });
156
461
  rl.on("line", async (line) => {
157
462
  let req;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everme/claude-code",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "EverMe native plugin for Claude Code — automatic memory recall via SessionStart/UserPromptSubmit/Stop/SessionEnd hooks, plus /recall slash + bundled MCP server.",
6
6
  "license": "Apache-2.0",
@@ -8,7 +8,7 @@
8
8
  "node": ">=18.0.0"
9
9
  },
10
10
  "scripts": {
11
- "test": "node --test tests/redact.test.js tests/config.test.js tests/transcript.test.js tests/hooks.test.js tests/mcp-server.test.js"
11
+ "test": "node --test tests/redact.test.js tests/config.test.js tests/transcript.test.js tests/hooks.test.js tests/mcp-server.test.js tests/skills.test.js"
12
12
  },
13
13
  "files": [
14
14
  ".claude-plugin/",
@@ -21,7 +21,7 @@
21
21
  "README.md"
22
22
  ],
23
23
  "dependencies": {
24
- "@everme/agent-sdk": "^0.4.1"
24
+ "@everme/agent-sdk": "^0.5.0"
25
25
  },
26
26
  "keywords": [
27
27
  "evermind",
@@ -1,30 +1,32 @@
1
1
  ---
2
- description: How and when to use EverMe memory tools to bring past-session context into the current Claude Code conversation.
2
+ description: Use EverMe memory proactively when the user refers to previous conversations, earlier decisions, "last time", "remember when", existing project conventions, or previously solved errors, and save durable user preferences, habits, and decisions the moment they are stated. Do not repeat a search when a non-empty <everme_recall> block already exists.
3
3
  alwaysInclude: true
4
4
  ---
5
5
 
6
6
  # EverMe Memory Tools
7
7
 
8
- You have two MCP tools that surface memory persisted by EverMe across past Claude Code sessions:
8
+ You have four canonical MCP tools for memory EverMe persists across past Claude Code sessions:
9
9
 
10
- - `everme_search` — semantic + keyword hybrid search over the user's memory store. Returns ranked items with subject, summary, score.
11
- - `everme_context` — fetch a server-rendered context block (profile + recent episodes) ready to inject into the current turn.
10
+ Recall:
11
+ - `mem_search` — semantic + keyword hybrid search over the user's memory store (episodic, profile, agent cases/skills, recent raw transcript). Rows under "Recent unextracted transcript" are provisional, not established facts.
12
+ - `mem_context` — the user's durable Profile snapshot ONLY. It never searches and never returns episodes; do not use it to recall past decisions or task context.
12
13
 
13
- The plugin's UserPromptSubmit hook already injects relevant memory automatically before each prompt. You usually do NOT need to call these tools manually — they're for cases the auto-recall missed.
14
+ Write:
15
+ - `mem_save_fact` — save a durable user fact (preference, habit, trait, long-term decision). Call it proactively the moment the user states one ("以后文档签名用 Alice", "I hate Friday meetings") — do NOT wait for the user to say "remember this".
16
+ - `mem_save_turn` — persist a complete task trajectory worth reusing. Chat-dual-write backends may also update the user's Profile; check `profileUpdated`. Use `mem_save_fact` for a deliberate durable fact.
14
17
 
15
- ## When to use these tools
18
+ ## Dedupe protocol (hooks come first)
16
19
 
17
- **Do call** when:
18
- - The user references something they discussed before ("last time", "remember when", "we decided to use X")
19
- - The user asks about a project pattern, decision, or convention you have no inline context for
20
- - You're debugging an error message that may have been seen + resolved before
21
- - The auto-recall block (`<everme_recall>...</everme_recall>` in your context) is empty or clearly unrelated to the current task
22
- - The user explicitly asks you to "search my memory" / "recall" / "look up"
20
+ The plugin's native hooks already inject `<everme_profile>` at session start and `<everme_recall>` before each prompt, and they save the conversation automatically. So:
23
21
 
24
- **Do NOT call** when:
25
- - The current message is self-contained and you can answer from inline context
26
- - You already searched in the current turn (don't duplicate)
27
- - It's a general-knowledge question with no project history component
22
+ - If this turn already carries a non-empty, relevant `<everme_recall>` block — do NOT call `mem_search` for the same topic.
23
+ - If the recall block is missing, empty, or clearly unrelated AND the task depends on history ("last time", "we decided", "did we fix this before", project conventions, previously solved errors) — call `mem_search` once, with a SHORT topic query, not the whole user message.
24
+ - Never repeat an identical query within the same turn.
25
+ - Call `mem_context` only when no `<everme_profile>` block was injected this session.
26
+
27
+ ## Save honesty
28
+
29
+ `mem_save_fact` returns the real extraction verdict. Only `extracted: true` / `profileUpdated: true` means the profile updated — then you may tell the user the fact is remembered. On `status: "no_extraction"` the profile did NOT update: say so plainly, do not auto-retry, and do not claim success.
28
30
 
29
31
  ## Best practices
30
32