@everme/claude-code 0.3.3 → 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.
@@ -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.3.3",
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.3",
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 recent context (profile + episodes) from past sessions and injects it as `additionalContext` for the model + a one-line `🧠 EverMe loaded N memory items` system message for you.
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** → after each Claude reply, persists the just-finished raw turn (including tool calls/results) to `/mem/agent-memory`.
10
- - **SessionEnd** → no persistence; Stop owns runtime writes so Claude Code does not create `/mem/sources`.
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 | Buffer flushes by turn-count or byte threshold | Every Stop event via realtime agent memory |
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
- | `EVERME_DEBUG` | `1` to print hook traces to stderr (with token redaction). |
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/api.js Gateway HTTP client (agent-memory, search, context)
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
@@ -26,6 +26,4 @@ Slash:
26
26
  MCP tools:
27
27
  everme_search — ranked search
28
28
  everme_context — server-rendered context block
29
-
30
- Tip: Set EVERME_DEBUG=1 to see hook traces on stderr.
31
29
  ```
@@ -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 { buildMemoryPrompt } from "@everme/agent-sdk";
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
- const MIN_PROMPT_WORDS = 3;
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
+ };
@@ -108,7 +108,7 @@ export function getConfig() {
108
108
  apiBase: process.env.EVERME_API_BASE,
109
109
  agentId: process.env.EVERME_AGENT_ID,
110
110
  agentToken,
111
- topK: 5,
111
+ topK: 10,
112
112
  });
113
113
 
114
114
  cached = {
@@ -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 { buildMemoryPrompt } from "@everme/agent-sdk";
22
- import { searchMemories, getContext, EvermeError } from "./lib/api.js";
23
- import { isConfigured } from "./lib/config.js";
24
- import { renderProfileBlock } from "./lib/profile.js";
25
- import { redactError, debug } from "./lib/redact.js";
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 5).",
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 5)" },
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 searchMemories(String(args.query || ""), { topK });
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 getContext(String(args.query || ""), {});
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 { isConfigured } from "./lib/config.js";
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
- const TOP_K = 6;
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 { debug } from "./lib/redact.js";
6
+ import { runClaudeCodeHook } from "./lib/run-hook.js";
18
7
 
19
- async function main() {
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 { isConfigured, getConfig } from "./lib/config.js";
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
- const MIN_MESSAGES = 1;
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.3",
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.3.3"
24
+ "@everme/agent-sdk": "^0.4.0"
25
25
  },
26
26
  "keywords": [
27
27
  "evermind",
@@ -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
- }