@everme/claude-code 0.6.1 → 0.6.2
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/commands/everme-help.md +0 -2
- package/hooks/hooks.json +12 -0
- package/hooks/scripts/lib/adapter.js +42 -4
- package/hooks/scripts/lib/transcript.js +80 -3
- package/hooks/scripts/mcp-server.js +8 -244
- package/hooks/scripts/store-subagent-memories.js +5 -0
- package/package.json +2 -2
- package/skills/memory-tools.md +1 -10
|
@@ -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.6.
|
|
13
|
+
"version": "0.6.2",
|
|
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.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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
|
@@ -7,11 +7,12 @@ Automatic memory recall + persistence for Claude Code, backed by the EverMe gate
|
|
|
7
7
|
- **SessionStart** → loads the profile snapshot from past sessions and injects it as `additionalContext` for the model + a one-line `🧠 EverMe loaded N memory items` system message for you.
|
|
8
8
|
- **UserPromptSubmit** → searches your memory for content relevant to the prompt you just typed and injects it BEFORE the model sees the prompt. Silent when no relevant hit (no nag).
|
|
9
9
|
- **Stop** → persists the just-finished raw turn (including tool calls/results) with `flush:false`; every fifth turn triggers extraction.
|
|
10
|
+
- **SubagentStop** → persists the child transcript's assistant/tool trajectory under a stable child conversation id; internal task/user wrappers are excluded and a per-transcript checkpoint prevents repeats.
|
|
10
11
|
- **SessionEnd** → sends a flush-only request so short sessions still extract memory without repeating messages.
|
|
11
12
|
|
|
12
13
|
Plus:
|
|
13
14
|
|
|
14
|
-
- **MCP server** exposing the
|
|
15
|
+
- **MCP server** exposing the read-only `mem_search` / `mem_context` tools for explicit recall (saving is automatic via the hooks).
|
|
15
16
|
- **Slash commands** `/recall <q>` and `/everme-help`.
|
|
16
17
|
- **Skill** (`memory-tools`) that tells Claude when/how to use the search tool.
|
|
17
18
|
|
|
@@ -21,7 +22,7 @@ Plus:
|
|
|
21
22
|
|---|---|---|
|
|
22
23
|
| Format | Generic MCP server | Claude Code plugin (hooks + commands + skill + MCP) |
|
|
23
24
|
| Recall trigger | User must call MCP tool manually | Automatic on every UserPromptSubmit |
|
|
24
|
-
| Save trigger | Explicit MCP tool call |
|
|
25
|
+
| Save trigger | Explicit MCP tool call | Stop + SubagentStop adds; every fifth root turn / SessionEnd extraction flush |
|
|
25
26
|
| Auth | `EVERME_AGENT_TOKEN` (evt) | Recall supports `EVERME_API_KEY` or `EVERME_AGENT_TOKEN`; realtime writes require `EVERME_AGENT_TOKEN` + `EVERME_AGENT_ID` |
|
|
26
27
|
| Backend | EverMe gateway (`/api/v1/mem/*`) | Same — runtime writes use `/mem/agent-memory`, not `/mem/sources` |
|
|
27
28
|
|
|
@@ -84,9 +85,10 @@ In a new Claude Code session:
|
|
|
84
85
|
.claude-plugin/plugin.json plugin metadata
|
|
85
86
|
.claude-plugin/marketplace.json marketplace listing
|
|
86
87
|
.mcp.json MCP server registration
|
|
87
|
-
hooks/hooks.json SessionStart / UserPromptSubmit / Stop / SessionEnd wiring
|
|
88
|
+
hooks/hooks.json SessionStart / UserPromptSubmit / Stop / SubagentStop / SessionEnd wiring
|
|
88
89
|
hooks/scripts/inject-memories.js UserPromptSubmit handler
|
|
89
90
|
hooks/scripts/store-memories.js Stop handler
|
|
91
|
+
hooks/scripts/store-subagent-memories.js SubagentStop handler
|
|
90
92
|
hooks/scripts/session-start.js SessionStart handler
|
|
91
93
|
hooks/scripts/session-summary.js SessionEnd handler
|
|
92
94
|
hooks/scripts/mcp-server.js MCP server (canonical mem_* tools)
|
package/commands/everme-help.md
CHANGED
package/hooks/hooks.json
CHANGED
|
@@ -36,6 +36,18 @@
|
|
|
36
36
|
]
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
|
+
"SubagentStop": [
|
|
40
|
+
{
|
|
41
|
+
"matcher": "*",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/store-subagent-memories.js",
|
|
46
|
+
"timeout": 30
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
39
51
|
"SessionEnd": [
|
|
40
52
|
{
|
|
41
53
|
"matcher": "*",
|
|
@@ -11,14 +11,20 @@ export const claudeCodeAdapter = {
|
|
|
11
11
|
return process.env.EVERME_ENV_FILE_PATH || path.join(os.homedir(), ".claude", "everme.env");
|
|
12
12
|
},
|
|
13
13
|
|
|
14
|
-
normalizeInput(rawInput) {
|
|
14
|
+
normalizeInput(rawInput, event) {
|
|
15
|
+
const isSubagent = event === "SubagentStop";
|
|
16
|
+
const parentSessionId = rawInput?.session_id || "claude-code-session";
|
|
17
|
+
const agentId = rawInput?.agent_id || "subagent";
|
|
15
18
|
return {
|
|
16
|
-
sessionId:
|
|
17
|
-
transcriptPath:
|
|
19
|
+
sessionId: isSubagent ? `${parentSessionId}__${agentId}` : parentSessionId,
|
|
20
|
+
transcriptPath: isSubagent
|
|
21
|
+
? rawInput?.agent_transcript_path || ""
|
|
22
|
+
: rawInput?.transcript_path || "",
|
|
18
23
|
cwd: rawInput?.cwd || "",
|
|
19
24
|
prompt: rawInput?.prompt || "",
|
|
20
25
|
turnId: rawInput?.turn_id || "",
|
|
21
26
|
source: rawInput?.source || "",
|
|
27
|
+
isSubagent,
|
|
22
28
|
};
|
|
23
29
|
},
|
|
24
30
|
|
|
@@ -45,13 +51,38 @@ export const claudeCodeAdapter = {
|
|
|
45
51
|
|
|
46
52
|
async readLastTurn(input) {
|
|
47
53
|
if (!input?.transcriptPath) return [];
|
|
48
|
-
const messages = extractAgentMessages(await readTranscript(input.transcriptPath)
|
|
54
|
+
const messages = extractAgentMessages(await readTranscript(input.transcriptPath), {
|
|
55
|
+
subagent: input.isSubagent,
|
|
56
|
+
});
|
|
49
57
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
50
58
|
if (messages[index]?.role === "user") return messages.slice(index);
|
|
51
59
|
}
|
|
52
60
|
return messages;
|
|
53
61
|
},
|
|
54
62
|
|
|
63
|
+
async readStoreBatches(input, { checkpointStore } = {}) {
|
|
64
|
+
if (!input?.transcriptPath) return [];
|
|
65
|
+
const messages = extractAgentMessages(await readTranscript(input.transcriptPath), {
|
|
66
|
+
subagent: input.isSubagent,
|
|
67
|
+
});
|
|
68
|
+
const stateId = `claude-code:${input.sessionId}`;
|
|
69
|
+
const checkpoint = checkpointStore
|
|
70
|
+
? await checkpointStore.read(stateId)
|
|
71
|
+
: { initialized: false, uploadedCount: 0 };
|
|
72
|
+
let delta;
|
|
73
|
+
if (checkpoint.initialized && checkpoint.uploadedCount <= messages.length) {
|
|
74
|
+
delta = messages.slice(checkpoint.uploadedCount);
|
|
75
|
+
} else {
|
|
76
|
+
delta = input.isSubagent ? messages : lastRootTurn(messages);
|
|
77
|
+
}
|
|
78
|
+
if (!delta.length) return [];
|
|
79
|
+
return [{
|
|
80
|
+
conversationId: input.sessionId,
|
|
81
|
+
messages: delta,
|
|
82
|
+
checkpoint: { stateId, uploadedCount: messages.length },
|
|
83
|
+
}];
|
|
84
|
+
},
|
|
85
|
+
|
|
55
86
|
formatOutput(event, { block = "", count = 0 } = {}) {
|
|
56
87
|
if (!CONTEXT_EVENTS.has(event) || !block) return {};
|
|
57
88
|
const systemMessage = event === "SessionStart"
|
|
@@ -66,3 +97,10 @@ export const claudeCodeAdapter = {
|
|
|
66
97
|
};
|
|
67
98
|
},
|
|
68
99
|
};
|
|
100
|
+
|
|
101
|
+
function lastRootTurn(messages) {
|
|
102
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
103
|
+
if (messages[index]?.role === "user") return messages.slice(index);
|
|
104
|
+
}
|
|
105
|
+
return messages;
|
|
106
|
+
}
|
|
@@ -133,7 +133,7 @@ export function extractTurns(lines) {
|
|
|
133
133
|
return turns;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function extractAgentMessages(lines) {
|
|
136
|
+
export function extractAgentMessages(lines, { subagent = false } = {}) {
|
|
137
137
|
const messages = [];
|
|
138
138
|
for (const line of lines) {
|
|
139
139
|
let ev;
|
|
@@ -142,6 +142,7 @@ export function extractAgentMessages(lines) {
|
|
|
142
142
|
} catch {
|
|
143
143
|
continue;
|
|
144
144
|
}
|
|
145
|
+
if (isInternalEvent(ev, subagent)) continue;
|
|
145
146
|
const timestamp = normalizeTimestamp(ev.timestamp);
|
|
146
147
|
// Real Claude Code transcript schema is nested: each line is
|
|
147
148
|
// { "type":"user"|"assistant", "message":{role, content}, ... }
|
|
@@ -161,8 +162,11 @@ export function extractAgentMessages(lines) {
|
|
|
161
162
|
// multiple EverMe messages.
|
|
162
163
|
const toolResults = extractToolResults(rawContent, timestamp);
|
|
163
164
|
messages.push(...toolResults);
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
if (subagent) continue;
|
|
166
|
+
const text = normalizeClaudeUserText(textFromContent(rawContent));
|
|
167
|
+
if (text) {
|
|
168
|
+
messages.push({ role: AGENT_MEMORY_ROLES.USER, timestamp, content: text });
|
|
169
|
+
}
|
|
166
170
|
continue;
|
|
167
171
|
}
|
|
168
172
|
if (role === AGENT_MEMORY_ROLES.ASSISTANT) {
|
|
@@ -182,6 +186,79 @@ export function extractAgentMessages(lines) {
|
|
|
182
186
|
return messages;
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
function isInternalEvent(event, subagent) {
|
|
190
|
+
if (event?.isMeta === true) return true;
|
|
191
|
+
if (subagent) return false;
|
|
192
|
+
return event?.isSidechain === true
|
|
193
|
+
|| (typeof event?.agentId === "string" && event.agentId.length > 0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeClaudeUserText(text) {
|
|
197
|
+
let trimmed = String(text || "").trim();
|
|
198
|
+
const initialCommand = commandIntent(trimmed);
|
|
199
|
+
if (initialCommand) return initialCommand;
|
|
200
|
+
if (hasEnvelopePrefix(trimmed, "command-message")) return "";
|
|
201
|
+
|
|
202
|
+
const injectedTags = [
|
|
203
|
+
"system-reminder",
|
|
204
|
+
"preview-annotation-context",
|
|
205
|
+
"task-notification",
|
|
206
|
+
"local-command-caveat",
|
|
207
|
+
"local-command-stdout",
|
|
208
|
+
"bash-input",
|
|
209
|
+
"bash-stdout",
|
|
210
|
+
"command-name",
|
|
211
|
+
"turn_aborted",
|
|
212
|
+
];
|
|
213
|
+
for (;;) {
|
|
214
|
+
let stripped = false;
|
|
215
|
+
for (const tag of injectedTags) {
|
|
216
|
+
const remaining = stripLeadingEnvelope(trimmed, tag);
|
|
217
|
+
if (remaining !== null) {
|
|
218
|
+
trimmed = remaining;
|
|
219
|
+
stripped = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!stripped) break;
|
|
224
|
+
}
|
|
225
|
+
if (!trimmed) return "";
|
|
226
|
+
const command = commandIntent(trimmed);
|
|
227
|
+
if (command) return command;
|
|
228
|
+
return hasEnvelopePrefix(trimmed, "command-message") ? "" : trimmed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function commandIntent(text) {
|
|
232
|
+
if (!hasEnvelopePrefix(text, "command-message")) return "";
|
|
233
|
+
const name = envelopeValue(text, "command-name");
|
|
234
|
+
if (!name.startsWith("/")) return "";
|
|
235
|
+
const args = envelopeValue(text, "command-args");
|
|
236
|
+
return `${name} ${args}`.trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function stripLeadingEnvelope(text, tag) {
|
|
240
|
+
if (!hasEnvelopePrefix(text, tag)) return null;
|
|
241
|
+
const close = `</${tag}>`;
|
|
242
|
+
const end = text.indexOf(close);
|
|
243
|
+
if (end < 0) return null;
|
|
244
|
+
return text.slice(end + close.length).trim();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hasEnvelopePrefix(text, tag) {
|
|
248
|
+
if (!text.startsWith(`<${tag}`)) return false;
|
|
249
|
+
return [">", " ", "\t", "\n", "\r"].includes(text.at(tag.length + 1));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function envelopeValue(text, tag) {
|
|
253
|
+
const open = `<${tag}>`;
|
|
254
|
+
const close = `</${tag}>`;
|
|
255
|
+
const start = text.indexOf(open);
|
|
256
|
+
if (start < 0) return "";
|
|
257
|
+
const valueStart = start + open.length;
|
|
258
|
+
const end = text.indexOf(close, valueStart);
|
|
259
|
+
return end < 0 ? "" : text.slice(valueStart, end).trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
185
262
|
// extractToolResults pulls every `tool_result` block out of a CC user-
|
|
186
263
|
// envelope content array and converts each into an EverMe role=tool
|
|
187
264
|
// message. CC encodes tool_result as a content block inside a user
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP server bundled with the Claude Code plugin. Exposes the
|
|
4
|
-
* EverMe
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* MCP server bundled with the Claude Code plugin. Exposes the read-only
|
|
4
|
+
* EverMe tools (mem_search / mem_context — the same public ABI as
|
|
5
|
+
* @everme/memory-mcp and the Go hosted /mcp surface). Saving is automatic
|
|
6
|
+
* via the plugin's hooks; the MCP write tools were retired.
|
|
7
7
|
*
|
|
8
8
|
* Wire format: MCP stdio transport (JSON-RPC 2.0 framed by line).
|
|
9
9
|
* We hand-roll the tiny subset Claude Code uses rather than pulling
|
|
@@ -21,9 +21,6 @@ import {
|
|
|
21
21
|
createClient,
|
|
22
22
|
getContext,
|
|
23
23
|
searchMemory,
|
|
24
|
-
saveAgentMemory,
|
|
25
|
-
savePersonalMemory,
|
|
26
|
-
AGENT_MEMORY_ROLES,
|
|
27
24
|
redactError,
|
|
28
25
|
describeError,
|
|
29
26
|
EvermeError,
|
|
@@ -76,13 +73,11 @@ function getClient() {
|
|
|
76
73
|
const INSTRUCTIONS = [
|
|
77
74
|
"EverMe memory is connected. This plugin's native hooks already inject a",
|
|
78
75
|
"<everme_profile> block at session start and a <everme_recall> block before",
|
|
79
|
-
"each prompt, and they save the conversation automatically — so
|
|
80
|
-
"
|
|
81
|
-
"a trigger fires
|
|
76
|
+
"each prompt, and they save the conversation automatically — so saving needs",
|
|
77
|
+
"no tool call. These two READ tools are for the cases the hooks do not cover;",
|
|
78
|
+
"call them AUTONOMOUSLY when a trigger fires, never waiting to be asked.",
|
|
82
79
|
"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.
|
|
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).",
|
|
80
|
+
"2. No <everme_profile> block was injected this session — call `mem_context` once. It returns the durable Profile ONLY (no search, no episodes).",
|
|
86
81
|
].join("\n");
|
|
87
82
|
|
|
88
83
|
const TOOLS = [
|
|
@@ -152,149 +147,6 @@ const TOOLS = [
|
|
|
152
147
|
},
|
|
153
148
|
},
|
|
154
149
|
},
|
|
155
|
-
{
|
|
156
|
-
name: "mem_save_turn",
|
|
157
|
-
description:
|
|
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.",
|
|
172
|
-
inputSchema: {
|
|
173
|
-
type: "object",
|
|
174
|
-
properties: {
|
|
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
|
-
},
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
150
|
];
|
|
299
151
|
|
|
300
152
|
const handlers = {
|
|
@@ -341,78 +193,6 @@ const handlers = {
|
|
|
341
193
|
const text = ctx?.context || "_(no profile available — your EverMe account has no extracted memories yet)_";
|
|
342
194
|
return ok(appendRequestID(redactError(text), ctx?.requestId));
|
|
343
195
|
}
|
|
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
|
-
});
|
|
415
|
-
}
|
|
416
196
|
default:
|
|
417
197
|
return errResp(`unknown tool: ${name}`);
|
|
418
198
|
}
|
|
@@ -440,22 +220,6 @@ function appendRequestID(text, requestId) {
|
|
|
440
220
|
return `${text}\n\n_(requestId: ${requestId})_`;
|
|
441
221
|
}
|
|
442
222
|
|
|
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
223
|
|
|
460
224
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
461
225
|
rl.on("line", async (line) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everme/claude-code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@everme/agent-sdk": "^0.6.
|
|
24
|
+
"@everme/agent-sdk": "^0.6.2"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"evermind",
|
package/skills/memory-tools.md
CHANGED
|
@@ -5,16 +5,11 @@ alwaysInclude: true
|
|
|
5
5
|
|
|
6
6
|
# EverMe Memory Tools
|
|
7
7
|
|
|
8
|
-
You have
|
|
8
|
+
You have two MCP tools for memory EverMe persists across past Claude Code sessions. Both are read-only; the native hooks own every write.
|
|
9
9
|
|
|
10
|
-
Recall:
|
|
11
10
|
- `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
11
|
- `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.
|
|
13
12
|
|
|
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.
|
|
17
|
-
|
|
18
13
|
## Dedupe protocol (hooks come first)
|
|
19
14
|
|
|
20
15
|
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:
|
|
@@ -24,10 +19,6 @@ The plugin's native hooks already inject `<everme_profile>` at session start and
|
|
|
24
19
|
- Never repeat an identical query within the same turn.
|
|
25
20
|
- Call `mem_context` only when no `<everme_profile>` block was injected this session.
|
|
26
21
|
|
|
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.
|
|
30
|
-
|
|
31
22
|
## Best practices
|
|
32
23
|
|
|
33
24
|
1. Search with the user's specific terms first; only broaden if zero hits.
|