@echomem/mcp 1.4.40 → 1.4.42

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/README.md CHANGED
@@ -74,6 +74,27 @@ installation, and `npx -y @echomem/mcp@latest update --all` remains the bootstra
74
74
  Agents can still call `echo_context_health` for an on-demand local context-health report. It reads
75
75
  the local Codex/Claude logs and does not require a separate process or desktop overlay.
76
76
 
77
+ ## Local conversation lifecycle hooks
78
+
79
+ For hook-capable Codex and Claude Code clients, MCP setup installs two local lifecycle hooks:
80
+
81
+ - **`SessionStart` — source-session binding.** The hook receives the host-owned `session_id` and
82
+ `transcript_path`, verifies that they identify the same local JSONL, and idempotently binds the
83
+ canonical `codex:<id>` or `claude-code:<id>` context through
84
+ `POST /api/extension/source-sessions/bind`. On each MCP call, the bridge independently confirms
85
+ the exact host-owned session metadata and attaches the returned context before saving, recalling,
86
+ citing, sharing, or recording activity. No "latest session" lookup is used. `bind_source_session`
87
+ remains available only for clients without startup hooks.
88
+ - **`Stop` — private-save checkpoint.** Before a completed turn exits, the hook reads only that
89
+ turn's local transcript slice. If the turn appears durable and did not already call
90
+ `save_conversation`, it blocks completion once and asks the Agent to create a concise private
91
+ checkpoint. It never authors or uploads a checkpoint itself. Re-entry is guarded by
92
+ `stop_hook_active`, so the second Stop is allowed after the Agent has checked or saved.
93
+
94
+ Both hook entries are merged idempotently into `~/.codex/hooks.json` and
95
+ `~/.claude/settings.json`; unrelated user hooks and settings are preserved. Hooks fail open when
96
+ their local input is missing so they cannot prevent a conversation from starting or finishing.
97
+
77
98
  ## Agent Doctor — local workspace forensics (new in 1.4.9)
78
99
 
79
100
  `echomem-mcp init` / `setup` now ends with an **Agent Doctor report**: a local scan of your
package/dist/hud/cli.js CHANGED
@@ -5,12 +5,13 @@ import { fileURLToPath } from "node:url";
5
5
  import { adapterList } from "./adapters.js";
6
6
  import { autostartPlistPath, autostartSupported, disableAutostart, enableAutostart, isAutostartEnabled } from "./autostart.js";
7
7
  import { statSignature } from "./fs.js";
8
- import { installHooks, installSaveCheckpointHooks } from "./hooks.js";
8
+ import { installHooks, installSaveCheckpointHooks, installSourceSessionHooks } from "./hooks.js";
9
9
  import { HudMonitor } from "./monitor.js";
10
10
  import { renderStateText } from "./render.js";
11
11
  import { renderReportText, runReport } from "./report.js";
12
12
  import { createHudServer } from "./server.js";
13
13
  import { runSaveCheckpointHook } from "../save-checkpoint-hook.js";
14
+ import { runSourceSessionStartHook } from "../source-session-hook.js";
14
15
  const argv = process.argv.slice(2);
15
16
  const command = argv[0] || "summary";
16
17
  const flags = parseFlags(argv.slice(1));
@@ -27,8 +28,12 @@ try {
27
28
  await cmdInstallHooks(flags);
28
29
  else if (command === "install-save-hooks")
29
30
  await cmdInstallSaveHooks(flags);
31
+ else if (command === "install-source-session-hooks")
32
+ await cmdInstallSourceSessionHooks(flags);
30
33
  else if (command === "save-checkpoint")
31
34
  await cmdSaveCheckpoint();
35
+ else if (command === "bind-source-session")
36
+ await cmdBindSourceSession();
32
37
  else if (command === "autostart")
33
38
  cmdAutostart(argv[1] || "status", flags);
34
39
  else if (command === "status")
@@ -120,8 +125,17 @@ async function cmdInstallHooks(flags) {
120
125
  console.log("Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
121
126
  }
122
127
  async function cmdInstallSaveHooks(flags) {
123
- const paths = installSaveCheckpointHooks(parseMode(flags.client));
124
- console.log(`Installed EchoMem private-save checkpoint hooks:\n${paths.map((p) => `- ${p}`).join("\n")}`);
128
+ const mode = parseMode(flags.client);
129
+ const paths = [...new Set([
130
+ ...installSourceSessionHooks(mode),
131
+ ...installSaveCheckpointHooks(mode),
132
+ ])];
133
+ console.log(`Installed EchoMem source-session and private-save hooks:\n${paths.map((p) => `- ${p}`).join("\n")}`);
134
+ console.log("Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
135
+ }
136
+ async function cmdInstallSourceSessionHooks(flags) {
137
+ const paths = installSourceSessionHooks(parseMode(flags.client));
138
+ console.log(`Installed EchoMem source-session hooks:\n${paths.map((p) => `- ${p}`).join("\n")}`);
125
139
  console.log("Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
126
140
  }
127
141
  async function cmdSaveCheckpoint() {
@@ -130,6 +144,12 @@ async function cmdSaveCheckpoint() {
130
144
  input += String(chunk);
131
145
  process.stdout.write(runSaveCheckpointHook(input));
132
146
  }
147
+ async function cmdBindSourceSession() {
148
+ let input = "";
149
+ for await (const chunk of process.stdin)
150
+ input += String(chunk);
151
+ process.stdout.write(await runSourceSessionStartHook(input));
152
+ }
133
153
  async function cmdReport(flags) {
134
154
  const result = runReport(parseMode(flags.client), { limit: readNumber(flags.limit, 40) });
135
155
  if (flags.json)
@@ -182,7 +202,8 @@ Usage:
182
202
  echomem-hud serve [--client codex|claude-code|claude-desktop|both|auto] [--port 17377]
183
203
  echomem-hud app [--client codex|claude-code|claude-desktop|both|auto]
184
204
  echomem-hud install-hooks [--client codex|claude-code|both]
185
- echomem-hud install-save-hooks [--client codex|claude-code|both]
205
+ echomem-hud install-save-hooks [--client codex|claude-code|both] (installs both lifecycle hooks)
206
+ echomem-hud install-source-session-hooks [--client codex|claude-code|both]
186
207
  echomem-hud autostart on|off|status (show the HUD after restart — macOS)
187
208
  echomem-hud status
188
209
  echomem-hud report [--client codex|claude-code|claude-desktop|auto] [--limit 40] [--json]
package/dist/hud/hooks.js CHANGED
@@ -22,6 +22,16 @@ export function installSaveCheckpointHooks(mode) {
22
22
  }
23
23
  return written;
24
24
  }
25
+ export function installSourceSessionHooks(mode) {
26
+ const written = [];
27
+ if (mode === "codex" || mode === "both" || mode === "auto") {
28
+ written.push(installCodexSourceSessionHook());
29
+ }
30
+ if (mode === "claude-code" || mode === "both" || mode === "auto") {
31
+ written.push(installClaudeCodeSourceSessionHook());
32
+ }
33
+ return written;
34
+ }
25
35
  function installCodexHooks() {
26
36
  const dir = path.join(os.homedir(), ".codex");
27
37
  const file = path.join(dir, "hooks.json");
@@ -39,6 +49,44 @@ function saveCheckpointCommand() {
39
49
  const lifecycleCli = fileURLToPath(new URL("./cli.js", import.meta.url));
40
50
  return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} save-checkpoint`;
41
51
  }
52
+ function sourceSessionCommand() {
53
+ const lifecycleCli = fileURLToPath(new URL("./cli.js", import.meta.url));
54
+ return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} bind-source-session`;
55
+ }
56
+ function installCodexSourceSessionHook() {
57
+ const dir = path.join(os.homedir(), ".codex");
58
+ const file = path.join(dir, "hooks.json");
59
+ fs.mkdirSync(dir, { recursive: true });
60
+ const content = readHooksFile(file);
61
+ content.hooks = content.hooks || {};
62
+ content.hooks.SessionStart = mergeSourceSessionGroup(content.hooks.SessionStart, {
63
+ hooks: [{
64
+ type: "command",
65
+ command: sourceSessionCommand(),
66
+ timeout: 5,
67
+ statusMessage: "Binding EchoMem to this conversation",
68
+ }],
69
+ });
70
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
71
+ return file;
72
+ }
73
+ function installClaudeCodeSourceSessionHook() {
74
+ const dir = path.join(os.homedir(), ".claude");
75
+ const file = path.join(dir, "settings.json");
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ const content = readHooksFile(file);
78
+ content.hooks = content.hooks || {};
79
+ content.hooks.SessionStart = mergeSourceSessionGroup(content.hooks.SessionStart, {
80
+ hooks: [{
81
+ type: "command",
82
+ command: sourceSessionCommand(),
83
+ timeout: 5,
84
+ statusMessage: "Binding EchoMem to this conversation",
85
+ }],
86
+ });
87
+ fs.writeFileSync(file, JSON.stringify(content, null, 2));
88
+ return file;
89
+ }
42
90
  function installCodexSaveCheckpointHook() {
43
91
  const dir = path.join(os.homedir(), ".codex");
44
92
  const file = path.join(dir, "hooks.json");
@@ -97,11 +145,21 @@ function mergeSaveCheckpointGroup(existing, group) {
97
145
  groups.push(group);
98
146
  return groups;
99
147
  }
148
+ function mergeSourceSessionGroup(existing, group) {
149
+ const groups = Array.isArray(existing) ? existing.filter((item) => !isEchoSourceSessionGroup(item)) : [];
150
+ groups.push(group);
151
+ return groups;
152
+ }
100
153
  function isEchoHudGroup(value) {
101
154
  if (typeof value !== "object" || value === null)
102
155
  return false;
103
156
  return JSON.stringify(value).includes("summary --client codex --json");
104
157
  }
158
+ function isEchoSourceSessionGroup(value) {
159
+ if (typeof value !== "object" || value === null)
160
+ return false;
161
+ return JSON.stringify(value).includes("bind-source-session");
162
+ }
105
163
  function isEchoSaveCheckpointGroup(value) {
106
164
  if (typeof value !== "object" || value === null)
107
165
  return false;