@everme/claude-code 0.3.4 → 0.4.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -7
- package/commands/everme-help.md +0 -2
- package/hooks/scripts/inject-memories.js +2 -135
- package/hooks/scripts/lib/adapter.js +68 -0
- package/hooks/scripts/lib/config.js +1 -1
- package/hooks/scripts/lib/run-hook.js +18 -0
- package/hooks/scripts/mcp-server.js +19 -12
- package/hooks/scripts/session-start.js +2 -64
- package/hooks/scripts/session-summary.js +2 -18
- package/hooks/scripts/store-memories.js +2 -116
- package/package.json +2 -2
- package/hooks/scripts/lib/api.js +0 -54
- package/hooks/scripts/lib/profile.js +0 -75
- package/hooks/scripts/lib/redact.js +0 -43
|
@@ -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.
|
|
13
|
+
"version": "0.4.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.
|
|
3
|
+
"version": "0.4.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
|
@@ -4,10 +4,10 @@ Automatic memory recall + persistence for Claude Code, backed by the EverMe gate
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
- **SessionStart** → loads
|
|
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
|
-
- **Stop** →
|
|
10
|
-
- **SessionEnd** →
|
|
9
|
+
- **Stop** → persists the just-finished raw turn (including tool calls/results) with `flush:false`; every fifth turn triggers extraction.
|
|
10
|
+
- **SessionEnd** → sends a flush-only request so short sessions still extract memory without repeating messages.
|
|
11
11
|
|
|
12
12
|
Plus:
|
|
13
13
|
|
|
@@ -21,7 +21,7 @@ Plus:
|
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| Format | Generic MCP server | Claude Code plugin (hooks + commands + skill + MCP) |
|
|
23
23
|
| Recall trigger | User must call MCP tool manually | Automatic on every UserPromptSubmit |
|
|
24
|
-
| Save trigger |
|
|
24
|
+
| Save trigger | Explicit MCP tool call | Every Stop add + every fifth turn / SessionEnd extraction flush |
|
|
25
25
|
| Auth | `EVERME_AGENT_TOKEN` (evt) | Recall supports `EVERME_API_KEY` or `EVERME_AGENT_TOKEN`; realtime writes require `EVERME_AGENT_TOKEN` + `EVERME_AGENT_ID` |
|
|
26
26
|
| Backend | EverMe gateway (`/api/v1/mem/*`) | Same — runtime writes use `/mem/agent-memory`, not `/mem/sources` |
|
|
27
27
|
|
|
@@ -55,7 +55,12 @@ claude plugin install /path/to/everme/plugins/claude-code
|
|
|
55
55
|
| `EVERME_AGENT_TOKEN` | Per-machine evt — written by `evercli plugin install claude-code`. Required for realtime writes and wins over emk when both are set. |
|
|
56
56
|
| `EVERME_AGENT_ID` | Required with `EVERME_AGENT_TOKEN` for realtime writes; also pins recall to a specific cloud agent. |
|
|
57
57
|
| `EVERME_API_BASE` | Gateway host. Defaults to `https://api.everme.evermind.ai`. Set to `http://localhost:8080` for local dev. |
|
|
58
|
-
| `
|
|
58
|
+
| `EVERME_INJECT_TOPK` | Recall rows, default `10`, clamped to `1..20`. |
|
|
59
|
+
| `EVERME_INJECT_PROFILE` | `1` includes profiles in per-prompt recall; default `0` because SessionStart already injects profile. |
|
|
60
|
+
| `EVERME_INJECT_MIN_SCORE` | Positive-score cutoff, default `0.1`; unscored rows remain eligible. |
|
|
61
|
+
| `EVERME_FLUSH_EVERY_TURNS` | Extraction cadence, default `5`; `0` disables cadence flush. |
|
|
62
|
+
| `EVERME_FLUSH_MODE` | Set `legacy` to restore every-turn flush. |
|
|
63
|
+
| `EVERME_STATE_DIR` | Turn counter directory, default `~/.everme/state`; files are `0600`. |
|
|
59
64
|
|
|
60
65
|
## Verifying the install
|
|
61
66
|
|
|
@@ -85,10 +90,10 @@ hooks/scripts/store-memories.js Stop handler
|
|
|
85
90
|
hooks/scripts/session-start.js SessionStart handler
|
|
86
91
|
hooks/scripts/session-summary.js SessionEnd handler
|
|
87
92
|
hooks/scripts/mcp-server.js MCP server (everme_search / everme_context tools)
|
|
88
|
-
hooks/scripts/lib/
|
|
93
|
+
hooks/scripts/lib/adapter.js Claude Code stdin/transcript/stdout adapter
|
|
94
|
+
hooks/scripts/lib/run-hook.js thin shared-runtime entry helper
|
|
89
95
|
hooks/scripts/lib/config.js Env-var resolution (emk vs evt)
|
|
90
96
|
hooks/scripts/lib/transcript.js Claude Code transcript JSONL reader
|
|
91
|
-
hooks/scripts/lib/redact.js evt/emk/X-Amz-Signature scrub for logs
|
|
92
97
|
commands/recall.md /recall slash command
|
|
93
98
|
commands/everme-help.md /everme-help slash command
|
|
94
99
|
skills/memory-tools.md always-injected skill — tells Claude how to use the tools
|
package/commands/everme-help.md
CHANGED
|
@@ -1,141 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* UserPromptSubmit hook — recall relevant memories and inject them
|
|
4
|
-
* into Claude's context BEFORE it sees the user's prompt.
|
|
5
|
-
*
|
|
6
|
-
* Hook contract (from Claude Code):
|
|
7
|
-
* stdin = JSON { prompt, transcript_path, cwd, ... }
|
|
8
|
-
* stdout = JSON {
|
|
9
|
-
* systemMessage: "...", // shown to user inline
|
|
10
|
-
* hookSpecificOutput: {
|
|
11
|
-
* hookEventName: "UserPromptSubmit",
|
|
12
|
-
* additionalContext: "..." // injected for the model only
|
|
13
|
-
* }
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* On any error or "no relevant memories" we exit 0 silently — never
|
|
17
|
-
* block the user's prompt on a memory backend hiccup.
|
|
18
|
-
*/
|
|
19
2
|
|
|
20
3
|
process.on("uncaughtException", () => process.exit(0));
|
|
21
4
|
process.on("unhandledRejection", () => process.exit(0));
|
|
22
5
|
|
|
23
|
-
import {
|
|
24
|
-
import { isConfigured } from "./lib/config.js";
|
|
25
|
-
import { searchMemories } from "./lib/api.js";
|
|
26
|
-
import { redactError, debug } from "./lib/redact.js";
|
|
6
|
+
import { runClaudeCodeHook } from "./lib/run-hook.js";
|
|
27
7
|
|
|
28
|
-
|
|
29
|
-
const TOP_K = 5;
|
|
30
|
-
const MIN_SCORE = 0.1;
|
|
31
|
-
|
|
32
|
-
async function main() {
|
|
33
|
-
const data = await readStdinJSON();
|
|
34
|
-
const prompt = String(data?.prompt || "");
|
|
35
|
-
if (!isConfigured()) {
|
|
36
|
-
debug("inject", "skip: not configured");
|
|
37
|
-
return process.exit(0);
|
|
38
|
-
}
|
|
39
|
-
if (countTokens(prompt) < MIN_PROMPT_WORDS) {
|
|
40
|
-
debug("inject", "skip: prompt too short");
|
|
41
|
-
return process.exit(0);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// /mem/search with the gateway's default memoryTypes returns episodes
|
|
45
|
-
// + profiles + raw_messages + agent_memory in a single call, ranked
|
|
46
|
-
// by query relevance. That's strictly more than /mem/context (a
|
|
47
|
-
// queryless full-profile snapshot) — context is kept for SessionStart
|
|
48
|
-
// where there is no prompt, but per-turn inject goes through search.
|
|
49
|
-
let block = "";
|
|
50
|
-
let count = 0;
|
|
51
|
-
let degraded = null;
|
|
52
|
-
try {
|
|
53
|
-
// The raw prompt can be a huge paste; the SDK's searchMemory clamps
|
|
54
|
-
// the query to the backend's MaxSearchQueryRunes limit, so we pass it
|
|
55
|
-
// through as-is rather than capping here too.
|
|
56
|
-
const res = await searchMemories(prompt, { topK: TOP_K });
|
|
57
|
-
// EverOS frequently returns score=null on episodic hits (decoded as
|
|
58
|
-
// 0 over the wire) — that's "unscored", not "score zero". Treat
|
|
59
|
-
// 0/null as unscored and keep the row; only drop rows with an
|
|
60
|
-
// explicit positive score below the threshold.
|
|
61
|
-
const filteredMemories = (res?.memories || []).filter((m) => {
|
|
62
|
-
const s = m?.score ?? m?.relevanceScore;
|
|
63
|
-
return s == null || s === 0 || s >= MIN_SCORE;
|
|
64
|
-
});
|
|
65
|
-
const bundle = {
|
|
66
|
-
memories: filteredMemories,
|
|
67
|
-
profiles: res?.profiles || [],
|
|
68
|
-
rawMessages: res?.rawMessages || [],
|
|
69
|
-
agentMemory: res?.agentMemory || { cases: [], skills: [] },
|
|
70
|
-
};
|
|
71
|
-
count =
|
|
72
|
-
filteredMemories.length +
|
|
73
|
-
bundle.profiles.length +
|
|
74
|
-
(bundle.agentMemory.cases?.length || 0) +
|
|
75
|
-
(bundle.agentMemory.skills?.length || 0) +
|
|
76
|
-
bundle.rawMessages.length;
|
|
77
|
-
// Reuse the SDK renderer so Claude Code and OpenClaw inject the
|
|
78
|
-
// same sectioned shape. wrapInCodeBlock=false because Claude Code
|
|
79
|
-
// wraps the body in an <everme_recall> envelope below — fenced
|
|
80
|
-
// markdown nested in XML reads worse than the bare section list.
|
|
81
|
-
const inner = buildMemoryPrompt(bundle, { wrapInCodeBlock: false });
|
|
82
|
-
if (inner) block = `<everme_recall>\n${inner}\n</everme_recall>`;
|
|
83
|
-
} catch (err) {
|
|
84
|
-
const reason = redactError(err?.message || String(err));
|
|
85
|
-
degraded = reason;
|
|
86
|
-
debug("inject", "search failed:", reason);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!block || count === 0) {
|
|
90
|
-
if (degraded) {
|
|
91
|
-
// Visible single-line WARN — the hook is by design non-blocking,
|
|
92
|
-
// but a fully silent failure means the user thinks EverMe is
|
|
93
|
-
// running while every recall is dropped. Surface it once per
|
|
94
|
-
// hook invocation so persistent issues (expired token, backend
|
|
95
|
-
// unreachable) bubble up without forcing EVERME_DEBUG=1.
|
|
96
|
-
process.stderr.write(`EverMe inject hook degraded: ${degraded}\n`);
|
|
97
|
-
} else {
|
|
98
|
-
debug("inject", "no memories above threshold");
|
|
99
|
-
}
|
|
100
|
-
return process.exit(0);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const systemMessage = `🧠 Recalling ${count} relevant ${count === 1 ? "memory" : "memories"} from EverMe`;
|
|
104
|
-
const out = {
|
|
105
|
-
systemMessage,
|
|
106
|
-
hookSpecificOutput: {
|
|
107
|
-
hookEventName: "UserPromptSubmit",
|
|
108
|
-
additionalContext: block,
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
process.stdout.write(JSON.stringify(out));
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Multilingual rough word count (CJK characters count individually,
|
|
116
|
-
// other languages by whitespace tokens). Mirrors the reference
|
|
117
|
-
// plugin's heuristic so users get the same min-prompt feel.
|
|
118
|
-
function countTokens(text) {
|
|
119
|
-
if (!text) return 0;
|
|
120
|
-
const cjkRe = /[一-鿿㐀-䶿-ゟ゠-ヿ가-]/g;
|
|
121
|
-
const cjk = (text.match(cjkRe) || []).length;
|
|
122
|
-
const ascii = text
|
|
123
|
-
.replace(cjkRe, " ")
|
|
124
|
-
.split(/\s+/)
|
|
125
|
-
.filter(Boolean).length;
|
|
126
|
-
return cjk + ascii;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function readStdinJSON() {
|
|
130
|
-
const chunks = [];
|
|
131
|
-
for await (const c of process.stdin) chunks.push(c);
|
|
132
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
133
|
-
if (!raw) return {};
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(raw);
|
|
136
|
-
} catch {
|
|
137
|
-
return {};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
main();
|
|
8
|
+
runClaudeCodeHook("UserPromptSubmit");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readTranscript, extractAgentMessages } from "./transcript.js";
|
|
4
|
+
|
|
5
|
+
const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit"]);
|
|
6
|
+
|
|
7
|
+
export const claudeCodeAdapter = {
|
|
8
|
+
platform: "claude-code",
|
|
9
|
+
|
|
10
|
+
envFile() {
|
|
11
|
+
return process.env.EVERME_ENV_FILE_PATH || path.join(os.homedir(), ".claude", "everme.env");
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
normalizeInput(rawInput) {
|
|
15
|
+
return {
|
|
16
|
+
sessionId: rawInput?.session_id || "claude-code-session",
|
|
17
|
+
transcriptPath: rawInput?.transcript_path || "",
|
|
18
|
+
cwd: rawInput?.cwd || "",
|
|
19
|
+
prompt: rawInput?.prompt || "",
|
|
20
|
+
turnId: rawInput?.turn_id || "",
|
|
21
|
+
source: rawInput?.source || "",
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Claude Code's documented Stop stdin carries no turn_id (only
|
|
26
|
+
// session_id / transcript_path / cwd / stop_hook_active), so keying dedup
|
|
27
|
+
// on rawInput.turn_id alone never fires. Derive a stable per-turn key
|
|
28
|
+
// from the transcript instead: the uuid of the last recorded event. A
|
|
29
|
+
// re-fired Stop over an unchanged transcript resolves to the same key and
|
|
30
|
+
// is deduped; a new turn appends new events and yields a new key. Returns
|
|
31
|
+
// "" (dedup disabled) when the transcript has no uuids.
|
|
32
|
+
async resolveTurnId(input) {
|
|
33
|
+
if (!input?.transcriptPath) return "";
|
|
34
|
+
const lines = await readTranscript(input.transcriptPath);
|
|
35
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
try {
|
|
37
|
+
const event = JSON.parse(lines[index]);
|
|
38
|
+
if (typeof event?.uuid === "string" && event.uuid) return event.uuid;
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async readLastTurn(input) {
|
|
47
|
+
if (!input?.transcriptPath) return [];
|
|
48
|
+
const messages = extractAgentMessages(await readTranscript(input.transcriptPath));
|
|
49
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
50
|
+
if (messages[index]?.role === "user") return messages.slice(index);
|
|
51
|
+
}
|
|
52
|
+
return messages;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
formatOutput(event, { block = "", count = 0 } = {}) {
|
|
56
|
+
if (!CONTEXT_EVENTS.has(event) || !block) return {};
|
|
57
|
+
const systemMessage = event === "SessionStart"
|
|
58
|
+
? `🧠 EverMe loaded ${count} memory ${count === 1 ? "item" : "items"} from past sessions`
|
|
59
|
+
: `🧠 Recalling ${count} relevant ${count === 1 ? "memory" : "memories"} from EverMe`;
|
|
60
|
+
return {
|
|
61
|
+
systemMessage,
|
|
62
|
+
hookSpecificOutput: {
|
|
63
|
+
hookEventName: event,
|
|
64
|
+
additionalContext: block,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runHook } from "@everme/agent-sdk";
|
|
2
|
+
import { claudeCodeAdapter } from "./adapter.js";
|
|
3
|
+
|
|
4
|
+
export async function runClaudeCodeHook(event) {
|
|
5
|
+
const output = await runHook(event, await readStdinJSON(), claudeCodeAdapter);
|
|
6
|
+
if (output && Object.keys(output).length) process.stdout.write(JSON.stringify(output));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function readStdinJSON() {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
12
|
+
if (!chunks.length) return {};
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -18,11 +18,15 @@
|
|
|
18
18
|
|
|
19
19
|
import { createInterface } from "readline";
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
import {
|
|
22
|
+
buildMemoryPrompt,
|
|
23
|
+
createClient,
|
|
24
|
+
searchMemory,
|
|
25
|
+
renderProfileBlock,
|
|
26
|
+
redactError,
|
|
27
|
+
EvermeError,
|
|
28
|
+
} from "@everme/agent-sdk";
|
|
29
|
+
import { getConfig, isConfigured } from "./lib/config.js";
|
|
26
30
|
|
|
27
31
|
// Derive serverInfo.version from package.json so the value tracks the
|
|
28
32
|
// plugin release rather than rotting as a hard-coded literal. We sit at
|
|
@@ -41,6 +45,12 @@ const { version: PKG_VERSION } = createRequire(import.meta.url)("../../package.j
|
|
|
41
45
|
// Claude Code already negotiates "2025-03-26".
|
|
42
46
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2024-11-05", "2025-03-26"]);
|
|
43
47
|
const LATEST_PROTOCOL_VERSION = "2025-03-26";
|
|
48
|
+
let client;
|
|
49
|
+
|
|
50
|
+
function getClient() {
|
|
51
|
+
if (!client) client = createClient(getConfig());
|
|
52
|
+
return client;
|
|
53
|
+
}
|
|
44
54
|
|
|
45
55
|
const TOOLS = [
|
|
46
56
|
{
|
|
@@ -59,12 +69,12 @@ const TOOLS = [
|
|
|
59
69
|
{
|
|
60
70
|
name: "everme_context",
|
|
61
71
|
description:
|
|
62
|
-
"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
|
|
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).",
|
|
63
73
|
inputSchema: {
|
|
64
74
|
type: "object",
|
|
65
75
|
properties: {
|
|
66
76
|
query: { type: "string", description: "Optional query for relevance-biased context" },
|
|
67
|
-
topK: { type: "number", description: "Max items to include (default
|
|
77
|
+
topK: { type: "number", description: "Max items to include (default 10)" },
|
|
68
78
|
},
|
|
69
79
|
},
|
|
70
80
|
},
|
|
@@ -99,7 +109,7 @@ const handlers = {
|
|
|
99
109
|
// host LLM to peel a JSON envelope and decode escaped
|
|
100
110
|
// newlines before any of the section bullets were readable.
|
|
101
111
|
const topK = Math.min(Number(args.topK) || 10, 25);
|
|
102
|
-
const res = await
|
|
112
|
+
const res = await searchMemory(getClient(), { query: String(args.query || ""), topK });
|
|
103
113
|
const body = buildMemoryPrompt(res, { wrapInCodeBlock: false });
|
|
104
114
|
const header = `## EverMe search results for "${String(args.query || "")}"`;
|
|
105
115
|
const trimmed = body.replace(/^## Relevant memory\n\n?/, "");
|
|
@@ -116,7 +126,7 @@ const handlers = {
|
|
|
116
126
|
// uses for the SessionStart hook injection) so the Tools
|
|
117
127
|
// path matches what users already see in the injected
|
|
118
128
|
// <everme_profile> block.
|
|
119
|
-
const res = await
|
|
129
|
+
const res = await getClient().request("POST", "/mem/context", {});
|
|
120
130
|
// renderProfileBlock returns "" when profile exists but has no
|
|
121
131
|
// facts/traits yet (new account). Check the rendered output, not
|
|
122
132
|
// just the wrapper object, so the empty case yields a fallback
|
|
@@ -130,7 +140,6 @@ const handlers = {
|
|
|
130
140
|
}
|
|
131
141
|
} catch (err) {
|
|
132
142
|
const safe = redactError(err instanceof EvermeError ? err.message : err?.message || String(err));
|
|
133
|
-
debug("mcp", `tool ${name} failed:`, safe);
|
|
134
143
|
return errResp(safe);
|
|
135
144
|
}
|
|
136
145
|
},
|
|
@@ -177,5 +186,3 @@ function respond(id, result, error) {
|
|
|
177
186
|
: { jsonrpc: "2.0", id, result };
|
|
178
187
|
process.stdout.write(JSON.stringify(env) + "\n");
|
|
179
188
|
}
|
|
180
|
-
|
|
181
|
-
debug("mcp", "everme MCP server ready");
|
|
@@ -1,70 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* SessionStart hook — surface a recent-context block when a new
|
|
4
|
-
* Claude Code session begins. Helps the user (and Claude) pick up
|
|
5
|
-
* where the previous session left off without manually pasting
|
|
6
|
-
* context.
|
|
7
|
-
*
|
|
8
|
-
* Hook contract:
|
|
9
|
-
* stdin = JSON { cwd, session_id, ... }
|
|
10
|
-
* stdout = JSON { systemMessage, hookSpecificOutput: { ..., additionalContext } }
|
|
11
|
-
*/
|
|
12
2
|
|
|
13
3
|
process.on("uncaughtException", () => process.exit(0));
|
|
14
4
|
process.on("unhandledRejection", () => process.exit(0));
|
|
15
5
|
|
|
16
|
-
import {
|
|
17
|
-
import { getContext } from "./lib/api.js";
|
|
18
|
-
import { redactError, debug } from "./lib/redact.js";
|
|
19
|
-
import { renderProfileBlock, profileItemCount } from "./lib/profile.js";
|
|
6
|
+
import { runClaudeCodeHook } from "./lib/run-hook.js";
|
|
20
7
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async function main() {
|
|
24
|
-
if (!isConfigured()) {
|
|
25
|
-
debug("start", "skip: not configured");
|
|
26
|
-
return process.exit(0);
|
|
27
|
-
}
|
|
28
|
-
await readStdinJSON(); // drain stdin (may be empty)
|
|
29
|
-
|
|
30
|
-
let block = "";
|
|
31
|
-
let count = 0;
|
|
32
|
-
try {
|
|
33
|
-
// Empty query — the gateway returns the user's profile snapshot.
|
|
34
|
-
// Shape: { profile: {explicit_info, implicit_traits, ...} }
|
|
35
|
-
const ctx = await getContext("", { topK: TOP_K });
|
|
36
|
-
block = renderProfileBlock(ctx?.profile);
|
|
37
|
-
count = profileItemCount(ctx?.profile);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
debug("start", "context failed:", redactError(err?.message));
|
|
40
|
-
return process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
if (!block || count === 0) {
|
|
43
|
-
debug("start", "no context");
|
|
44
|
-
return process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const out = {
|
|
48
|
-
systemMessage: `🧠 EverMe loaded ${count} memory ${count === 1 ? "item" : "items"} from past sessions`,
|
|
49
|
-
hookSpecificOutput: {
|
|
50
|
-
hookEventName: "SessionStart",
|
|
51
|
-
additionalContext: block,
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
process.stdout.write(JSON.stringify(out));
|
|
55
|
-
process.exit(0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function readStdinJSON() {
|
|
59
|
-
const chunks = [];
|
|
60
|
-
for await (const c of process.stdin) chunks.push(c);
|
|
61
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
62
|
-
if (!raw) return {};
|
|
63
|
-
try {
|
|
64
|
-
return JSON.parse(raw);
|
|
65
|
-
} catch {
|
|
66
|
-
return {};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
main();
|
|
8
|
+
runClaudeCodeHook("SessionStart");
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* SessionEnd hook — no runtime persistence here.
|
|
4
|
-
*
|
|
5
|
-
* Claude Code runtime memory is written turn-by-turn by the Stop hook through
|
|
6
|
-
* /mem/agent-memory. SessionEnd must not upload a markdown summary to
|
|
7
|
-
* /mem/sources, otherwise long-lived sessions create document sources.
|
|
8
|
-
*
|
|
9
|
-
* Hook contract:
|
|
10
|
-
* stdin = JSON { transcript_path, session_id, cwd, ... }
|
|
11
|
-
* stdout = empty (no UI surface needed)
|
|
12
|
-
*/
|
|
13
2
|
|
|
14
3
|
process.on("uncaughtException", () => process.exit(0));
|
|
15
4
|
process.on("unhandledRejection", () => process.exit(0));
|
|
16
5
|
|
|
17
|
-
import {
|
|
6
|
+
import { runClaudeCodeHook } from "./lib/run-hook.js";
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
debug("summary", "skip: runtime persistence is handled by Stop via /mem/agent-memory");
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
main();
|
|
8
|
+
runClaudeCodeHook("SessionEnd");
|
|
@@ -1,122 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Stop hook — fires after Claude finishes responding to a turn.
|
|
4
|
-
* We read the transcript JSONL Claude Code wrote, extract the
|
|
5
|
-
* just-completed raw turn, and POST it through the realtime gateway
|
|
6
|
-
* (/mem/agent-memory). Runtime turns must not create /mem/sources.
|
|
7
|
-
*
|
|
8
|
-
* Hook contract (from Claude Code):
|
|
9
|
-
* stdin = JSON { transcript_path, cwd, session_id, ... }
|
|
10
|
-
* stdout = empty (no need to surface anything to the user)
|
|
11
|
-
*
|
|
12
|
-
* Failure-mode: silent exit 0 — host must NEVER notice memory
|
|
13
|
-
* persistence is broken. The gateway has its own retry loop on the
|
|
14
|
-
* worker side, so a single dropped Stop event isn't catastrophic.
|
|
15
|
-
*/
|
|
16
2
|
|
|
17
3
|
process.on("uncaughtException", () => process.exit(0));
|
|
18
4
|
process.on("unhandledRejection", () => process.exit(0));
|
|
19
5
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
saveAgentMemory,
|
|
23
|
-
EvermeError,
|
|
24
|
-
} from "./lib/api.js";
|
|
25
|
-
import { AGENT_MEMORY_ROLES } from "@everme/agent-sdk";
|
|
26
|
-
import { readTranscript, extractAgentMessages } from "./lib/transcript.js";
|
|
27
|
-
import { redactError, debug } from "./lib/redact.js";
|
|
6
|
+
import { runClaudeCodeHook } from "./lib/run-hook.js";
|
|
28
7
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
async function main() {
|
|
32
|
-
if (!isConfigured()) {
|
|
33
|
-
debug("store", "skip: not configured");
|
|
34
|
-
return process.exit(0);
|
|
35
|
-
}
|
|
36
|
-
const cfg = getConfig();
|
|
37
|
-
if (cfg.authMode !== "evt" || !cfg.agentId) {
|
|
38
|
-
debug("store", "skip: realtime agent memory requires EVERME_AGENT_TOKEN + EVERME_AGENT_ID");
|
|
39
|
-
return process.exit(0);
|
|
40
|
-
}
|
|
41
|
-
const data = await readStdinJSON();
|
|
42
|
-
const transcriptPath = data?.transcript_path;
|
|
43
|
-
const sessionId = data?.session_id || "claude-code-session";
|
|
44
|
-
if (!transcriptPath) {
|
|
45
|
-
debug("store", "skip: no transcript_path");
|
|
46
|
-
return process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const lines = await readTranscript(transcriptPath);
|
|
50
|
-
const messages = extractAgentMessages(lines);
|
|
51
|
-
// Only persist the LAST user/assistant pair from this Stop event so
|
|
52
|
-
// we don't re-upload the entire history every turn — backend chains
|
|
53
|
-
// versions per documentKey, so each call appends.
|
|
54
|
-
const tail = lastTurn(messages);
|
|
55
|
-
if (tail.length < MIN_MESSAGES) {
|
|
56
|
-
debug("store", `skip: tail < ${MIN_MESSAGES} messages (got ${tail.length})`);
|
|
57
|
-
return process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
// Stop is the natural session-end signal for Claude Code; flush
|
|
62
|
-
// so EverOS extracts episodes/profiles instead of letting
|
|
63
|
-
// raw_messages accumulate indefinitely.
|
|
64
|
-
const res = await saveAgentMemory({
|
|
65
|
-
conversationId: sessionId,
|
|
66
|
-
messages: tail,
|
|
67
|
-
flush: true,
|
|
68
|
-
});
|
|
69
|
-
debug("store", `ok agent-memory status=${res?.status || "unknown"} flushed=${!!res?.flushed} messages=${res?.messageCount || tail.length}`);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
// Visible single-line WARN — Stop hook is non-blocking by design,
|
|
72
|
-
// but silently dropping every turn means a misconfigured machine
|
|
73
|
-
// looks healthy while EverMe never receives a write. Surface
|
|
74
|
-
// it once per Stop so persistent issues (401, network, schema
|
|
75
|
-
// drift) bubble up without forcing EVERME_DEBUG=1.
|
|
76
|
-
let reason;
|
|
77
|
-
if (err instanceof EvermeError) {
|
|
78
|
-
reason = `${err.type}: ${redactError(err.message)}`;
|
|
79
|
-
debug("store", `failed type=${err.type}:`, redactError(err.message));
|
|
80
|
-
} else {
|
|
81
|
-
reason = redactError(err?.message || String(err));
|
|
82
|
-
debug("store", "unexpected:", reason);
|
|
83
|
-
}
|
|
84
|
-
process.stderr.write(`EverMe store hook degraded: ${reason}\n`);
|
|
85
|
-
}
|
|
86
|
-
process.exit(0);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Take the latest user prompt + assistant reply pair (plus any
|
|
91
|
-
* tool/tool_result events that fall between them). The gateway's
|
|
92
|
-
* realtime write API receives only this delta — uploading the whole
|
|
93
|
-
* history would duplicate memories on every turn.
|
|
94
|
-
*/
|
|
95
|
-
function lastTurn(messages) {
|
|
96
|
-
if (messages.length === 0) return [];
|
|
97
|
-
// Walk backwards: take everything from the last `user` event
|
|
98
|
-
// through the end. That captures user → tool... → assistant flow.
|
|
99
|
-
let startIdx = -1;
|
|
100
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
101
|
-
if (messages[i].role === AGENT_MEMORY_ROLES.USER) {
|
|
102
|
-
startIdx = i;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (startIdx === -1) return messages;
|
|
107
|
-
return messages.slice(startIdx);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function readStdinJSON() {
|
|
111
|
-
const chunks = [];
|
|
112
|
-
for await (const c of process.stdin) chunks.push(c);
|
|
113
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
114
|
-
if (!raw) return {};
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(raw);
|
|
117
|
-
} catch {
|
|
118
|
-
return {};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
main();
|
|
8
|
+
runClaudeCodeHook("Stop");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everme/claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@everme/agent-sdk": "^0.
|
|
24
|
+
"@everme/agent-sdk": "^0.4.0"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"evermind",
|
package/hooks/scripts/lib/api.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thin wrapper around `@everme/agent-sdk` for the Claude Code hooks.
|
|
3
|
-
*
|
|
4
|
-
* Why this file still exists:
|
|
5
|
-
* - The hooks call `searchMemories(query, { topK })` / `getContext` /
|
|
6
|
-
* `saveAgentMemory` against an SDK-managed client created from our config.
|
|
7
|
-
* Centralising the client construction here keeps each hook script short.
|
|
8
|
-
* - Re-exports keep the rest of the codebase (and tests) stable —
|
|
9
|
-
* a future SDK rename would only land here.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
createClient,
|
|
14
|
-
searchMemory as sdkSearchMemory,
|
|
15
|
-
saveAgentMemory as sdkSaveAgentMemory,
|
|
16
|
-
EvermeError,
|
|
17
|
-
} from "@everme/agent-sdk";
|
|
18
|
-
import { getConfig } from "./config.js";
|
|
19
|
-
|
|
20
|
-
let _client = null;
|
|
21
|
-
|
|
22
|
-
function getClient() {
|
|
23
|
-
if (_client) return _client;
|
|
24
|
-
_client = createClient(getConfig());
|
|
25
|
-
return _client;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function searchMemories(query, opts = {}) {
|
|
29
|
-
// agent is bound from the MemAuth token; no agentId in the body.
|
|
30
|
-
return sdkSearchMemory(getClient(), { query, ...opts });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Direct POST /mem/context that returns the gateway's raw shape
|
|
35
|
-
* {profile, cachedAt, generatedAt}. The hooks render `profile`
|
|
36
|
-
* themselves (see inject-memories.js / session-start.js's
|
|
37
|
-
* renderProfileBlock). The SDK's `getContext` is too opinionated for
|
|
38
|
-
* our needs (it extracts a server-rendered .context string the
|
|
39
|
-
* gateway doesn't currently produce), so we bypass it and call the
|
|
40
|
-
* client directly.
|
|
41
|
-
*
|
|
42
|
-
* Body is `{ forceRefresh: bool }` only — the agent is bound from
|
|
43
|
-
* the MemAuth token, so `query`/`topK` are no longer forwarded.
|
|
44
|
-
*/
|
|
45
|
-
export async function getContext(_query, opts = {}) {
|
|
46
|
-
const body = opts.forceRefresh ? { forceRefresh: true } : {};
|
|
47
|
-
return getClient().request("POST", "/mem/context", body);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function saveAgentMemory(req, log) {
|
|
51
|
-
return sdkSaveAgentMemory(getClient(), req, log);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export { EvermeError };
|
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
}
|