@echomem/mcp 1.4.19 → 1.4.20

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.
@@ -88,6 +88,144 @@ export const SETUP_PAGE_STYLES_WEBSITE_ALIGNMENT = String.raw `
88
88
  box-shadow: none;
89
89
  }
90
90
 
91
+ /* Local scan -------------------------------------------------------------- */
92
+ body.scanMode {
93
+ background:
94
+ radial-gradient(circle, rgba(26,58,143,0.075) 0 1px, transparent 1.35px) 0 0 / 18px 18px,
95
+ radial-gradient(circle, rgba(78,205,196,0.07) 0 1px, transparent 1.35px) 9px 9px / 22px 22px,
96
+ var(--echo-paper-canvas);
97
+ }
98
+ body.scanMode main {
99
+ padding: clamp(32px, 6vw, 72px) clamp(18px, 5vw, 58px);
100
+ }
101
+ body.scanMode #app {
102
+ width: min(860px, 100%);
103
+ min-height: min(72vh, 680px);
104
+ }
105
+ .scanLoader {
106
+ position: relative;
107
+ overflow: visible;
108
+ width: min(760px, 100%);
109
+ border: 1.5px solid rgba(26,58,143,0.20);
110
+ border-radius: var(--echo-radius-panel);
111
+ background: rgba(255,255,255,0.92);
112
+ padding: clamp(26px, 5vw, 48px);
113
+ box-shadow: var(--echo-shadow-pop);
114
+ }
115
+ .scanLoader::before {
116
+ content: "";
117
+ position: absolute;
118
+ top: -11px;
119
+ left: 50%;
120
+ z-index: 2;
121
+ width: 88px;
122
+ height: 25px;
123
+ border: 1px solid rgba(43,42,38,0.07);
124
+ background: color-mix(in srgb, var(--echo-note-cream) 72%, white 28%);
125
+ box-shadow: 0 1px 2px rgba(43,42,38,0.07);
126
+ opacity: 0.82;
127
+ transform: translateX(-50%) rotate(-1deg);
128
+ }
129
+ .scanLoaderLead {
130
+ display: grid;
131
+ grid-template-columns: auto minmax(0, 1fr);
132
+ align-items: center;
133
+ gap: clamp(20px, 4vw, 34px);
134
+ }
135
+ .scanMascot {
136
+ display: grid;
137
+ place-items: center;
138
+ width: clamp(92px, 13vw, 118px);
139
+ aspect-ratio: 1;
140
+ flex: 0 0 auto;
141
+ border: 1px solid rgba(26,58,143,0.11);
142
+ border-radius: 20px;
143
+ background: color-mix(in srgb, var(--echo-paper-white) 64%, var(--echo-note-cream) 36%);
144
+ box-shadow: var(--echo-shadow-paper);
145
+ transform: rotate(-1.5deg);
146
+ }
147
+ .scanMascot img {
148
+ display: block;
149
+ width: 76%;
150
+ height: 76%;
151
+ object-fit: contain;
152
+ filter: drop-shadow(0 12px 14px rgba(26,58,143,0.16));
153
+ }
154
+ .scanLoaderCopy { min-width: 0; }
155
+ .scanEyebrow {
156
+ width: fit-content;
157
+ margin: 0 0 15px;
158
+ border: 1px solid rgba(26,58,143,0.13);
159
+ border-radius: 999px;
160
+ background: rgba(255,255,255,0.72);
161
+ padding: 6px 11px;
162
+ box-shadow: var(--echo-shadow-glass);
163
+ color: color-mix(in srgb, var(--echo-ink-primary) 70%, var(--echo-ink-accent) 30%);
164
+ font-size: 11px;
165
+ letter-spacing: 0.11em;
166
+ }
167
+ .scanLoader h2.siteHeadline {
168
+ max-width: 560px;
169
+ font-size: clamp(40px, 6vw, 64px);
170
+ line-height: 0.98;
171
+ }
172
+ .scanIntro {
173
+ max-width: 520px;
174
+ margin: 18px 0 0;
175
+ color: var(--echo-ink-mute);
176
+ font-size: 15px;
177
+ font-weight: 500;
178
+ line-height: 1.55;
179
+ }
180
+ .scanProgressCard {
181
+ margin-top: clamp(26px, 5vw, 38px);
182
+ border: 1px solid rgba(26,58,143,0.10);
183
+ border-radius: var(--echo-radius-card);
184
+ background: color-mix(in srgb, var(--echo-paper-white) 68%, var(--echo-note-blue) 32%);
185
+ padding: 17px 18px 15px;
186
+ box-shadow: var(--echo-shadow-paper);
187
+ }
188
+ .scanProgress {
189
+ width: 100%;
190
+ height: 10px;
191
+ margin: 0;
192
+ border-radius: 999px;
193
+ background: rgba(255,255,255,0.78);
194
+ box-shadow: inset 0 1px 3px rgba(20,24,34,0.10);
195
+ }
196
+ .scanProgressFill {
197
+ min-width: 10px;
198
+ border-radius: 999px;
199
+ background: linear-gradient(90deg, var(--echo-ink-primary), color-mix(in srgb, var(--echo-ink-primary) 72%, var(--echo-ink-accent) 28%));
200
+ box-shadow: 0 0 0 1px rgba(26,58,143,0.08), 0 3px 9px rgba(26,58,143,0.20);
201
+ }
202
+ .scanCaption {
203
+ margin: 12px 0 0;
204
+ color: var(--echo-ink-mute);
205
+ font-size: 12px;
206
+ font-weight: 700;
207
+ line-height: 1.5;
208
+ }
209
+ .scanPrivacy {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 8px;
213
+ margin: 15px 2px 0;
214
+ color: var(--echo-ink-faint);
215
+ font-family: var(--echo-font-mono);
216
+ font-size: 11px;
217
+ font-weight: 600;
218
+ line-height: 1.45;
219
+ }
220
+ .scanPrivacy > span {
221
+ width: 8px;
222
+ height: 8px;
223
+ flex: 0 0 auto;
224
+ border-radius: 50%;
225
+ background: var(--echo-ink-accent-deep);
226
+ box-shadow: 0 0 0 4px rgba(78,205,196,0.13);
227
+ }
228
+
91
229
  /* Permission and skip states ------------------------------------------------ */
92
230
  .localConsent {
93
231
  overflow: visible;
@@ -379,6 +517,9 @@ export const SETUP_PAGE_STYLES_WEBSITE_ALIGNMENT = String.raw `
379
517
  .readyDecision .actions .primary:not(:disabled):hover {
380
518
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.28), 0 16px 34px rgba(26,58,143,0.25);
381
519
  }
520
+ .readyDecision .actions .skipHistoryButton {
521
+ border-radius: 999px;
522
+ }
382
523
  .readyFoot {
383
524
  border-top-color: rgba(26,58,143,0.10);
384
525
  border-radius: 0 0 calc(var(--echo-radius-panel) - 2px) calc(var(--echo-radius-panel) - 2px);
@@ -563,6 +704,16 @@ export const SETUP_PAGE_STYLES_WEBSITE_ALIGNMENT = String.raw `
563
704
  background: color-mix(in srgb, var(--echo-paper-white) 64%, var(--echo-note-blue) 36%);
564
705
  box-shadow: inset 3px 0 0 var(--echo-ink-primary);
565
706
  }
707
+ .sessionPickerLimitCard {
708
+ border-color: rgba(26,58,143,0.16);
709
+ border-radius: var(--echo-radius-card);
710
+ background: color-mix(in srgb, rgba(255,255,255,0.94) 84%, var(--echo-note-cream) 16%);
711
+ box-shadow: var(--echo-shadow-pop);
712
+ }
713
+ .sessionPickerLimitActions .button,
714
+ .sessionPickerLimitActions button {
715
+ border-radius: 999px;
716
+ }
566
717
  .sessionPickerSource {
567
718
  border-radius: 9px;
568
719
  box-shadow: var(--echo-shadow-paper);
@@ -585,6 +736,38 @@ export const SETUP_PAGE_STYLES_WEBSITE_ALIGNMENT = String.raw `
585
736
  .skipStage {
586
737
  border-radius: 18px;
587
738
  }
739
+ body.scanMode main {
740
+ padding: 34px 14px 52px;
741
+ }
742
+ body.scanMode #app {
743
+ min-height: calc(100vh - 86px);
744
+ }
745
+ .scanLoader {
746
+ border-radius: 18px;
747
+ padding: 28px 20px 22px;
748
+ }
749
+ .scanLoaderLead {
750
+ grid-template-columns: 1fr;
751
+ gap: 18px;
752
+ }
753
+ .scanMascot {
754
+ width: 76px;
755
+ border-radius: 16px;
756
+ }
757
+ .scanLoader h2.siteHeadline {
758
+ font-size: clamp(38px, 12vw, 52px);
759
+ }
760
+ .scanIntro {
761
+ margin-top: 14px;
762
+ font-size: 14px;
763
+ }
764
+ .scanProgressCard {
765
+ margin-top: 24px;
766
+ padding: 15px 14px 13px;
767
+ }
768
+ .scanPrivacy {
769
+ align-items: flex-start;
770
+ }
588
771
  .consentLead,
589
772
  .skippedEcho {
590
773
  border-radius: 16px 16px 0 0;
@@ -1,9 +1,11 @@
1
1
  export const SETUP_PREVIEW_STATES = [
2
2
  "consent",
3
3
  "consent-required",
4
+ "scan",
4
5
  "report",
5
6
  "extract-counting",
6
7
  "extract-ready",
8
+ "extract-limit",
7
9
  "extract-run",
8
10
  "extract-overdue",
9
11
  "extract-done",
@@ -211,6 +213,17 @@ export function renderSetupPreviewBootstrap(state) {
211
213
  renderLocalScanConsent();
212
214
  setConsentStatus("Local history access is required to continue setup. Nothing will be scanned or uploaded unless you allow it.", "required");`;
213
215
  }
216
+ if (state === "scan") {
217
+ return `${watermark}
218
+ reportEnvelope = {
219
+ schemaVersion: 1,
220
+ kind: "scanning",
221
+ mode: "preview",
222
+ scanId: "design-preview",
223
+ progress: { stage: "classifying-sessions", label: "Classifying repeated context", scanned: 116, total: 180, stageElapsedMs: 3000 }
224
+ };
225
+ renderLoading();`;
226
+ }
214
227
  if (state === "report") {
215
228
  const reportJson = JSON.stringify(previewReport);
216
229
  return `${watermark}
@@ -223,7 +236,8 @@ export function renderSetupPreviewBootstrap(state) {
223
236
  stats = null;
224
237
  renderDashboard();`;
225
238
  }
226
- if (state === "extract-ready") {
239
+ if (state === "extract-ready" || state === "extract-limit") {
240
+ const remainingHistory = state === "extract-limit" ? 0 : 28;
227
241
  return `${watermark}
228
242
  connected = true;
229
243
  billingStatus = {
@@ -234,7 +248,7 @@ export function renderSetupPreviewBootstrap(state) {
234
248
  email: "erik@example.com",
235
249
  avatarUrl: "/hud-assets/echo-face-cutout.png"
236
250
  },
237
- historicalConversationQuota: { used: 72, limit: 100, remaining: 28 }
251
+ historicalConversationQuota: { used: ${100 - remainingHistory}, limit: 100, remaining: ${remainingHistory} }
238
252
  };
239
253
  stats = {
240
254
  sessions: { total: 136, codex: 82, claudeCode: 54 },
package/dist/setup.js CHANGED
@@ -39,9 +39,107 @@ import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "
39
39
  const WEB_URL = (process.env.ECHO_WEB_URL || "https://yeahecho.com").replace(/\/$/, "");
40
40
  const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
41
41
  const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
42
+ const CODEX_SKILL_NAMES = [
43
+ "echomem-search",
44
+ "echomem-save",
45
+ "echomem-forget",
46
+ "echomem-login",
47
+ ];
42
48
  function home(...p) {
43
49
  return path.join(os.homedir(), ...p);
44
50
  }
51
+ function codexHome() {
52
+ const configured = process.env.CODEX_HOME?.trim();
53
+ if (!configured)
54
+ return home(".codex");
55
+ if (configured === "~")
56
+ return os.homedir();
57
+ if (configured.startsWith(`~${path.sep}`))
58
+ return path.join(os.homedir(), configured.slice(2));
59
+ return path.resolve(configured);
60
+ }
61
+ function filesEqual(left, right) {
62
+ try {
63
+ return fs.readFileSync(left).equals(fs.readFileSync(right));
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function packagedSkillMatches(source, destination) {
70
+ return filesEqual(path.join(source, "SKILL.md"), path.join(destination, "SKILL.md"))
71
+ && filesEqual(path.join(source, "agents", "openai.yaml"), path.join(destination, "agents", "openai.yaml"));
72
+ }
73
+ function readSkillMetadata(skillFile) {
74
+ try {
75
+ const content = fs.readFileSync(skillFile, "utf8");
76
+ const frontmatter = content.match(/^---\s*\n([\s\S]*?)\n---/);
77
+ if (!frontmatter)
78
+ return null;
79
+ const name = frontmatter[1].match(/^name:\s*(.+)$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? "";
80
+ const description = frontmatter[1].match(/^description:\s*(.+)$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? "";
81
+ return name ? { name, description } : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ function detectCompetingMemorySkills(skillsRoot) {
88
+ let entries = [];
89
+ try {
90
+ entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ const echoNames = new Set(CODEX_SKILL_NAMES);
96
+ return entries
97
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
98
+ .map((entry) => readSkillMetadata(path.join(skillsRoot, entry.name, "SKILL.md")))
99
+ .filter((metadata) => Boolean(metadata))
100
+ .filter(({ name, description }) => {
101
+ if (echoNames.has(name))
102
+ return false;
103
+ const text = `${name} ${description}`;
104
+ return /\b(memory|memories|remember|recall)\b/i.test(text)
105
+ && /\b(search|save|forget|delete|login|connect|recall|remember)\b/i.test(text);
106
+ })
107
+ .map(({ name }) => name)
108
+ .sort();
109
+ }
110
+ /** Install the EchoMem-owned Codex skills bundled with this npm package. Other providers are never modified. */
111
+ export function installCodexSkills(targetCodexHome = codexHome(), templateRoot = fileURLToPath(new URL("../templates/codex-skills/", import.meta.url))) {
112
+ const skillsRoot = path.join(targetCodexHome, "skills");
113
+ const report = {
114
+ skillsRoot,
115
+ installed: [],
116
+ updated: [],
117
+ unchanged: [],
118
+ competingMemorySkills: [],
119
+ };
120
+ fs.mkdirSync(skillsRoot, { recursive: true });
121
+ for (const skillName of CODEX_SKILL_NAMES) {
122
+ const source = path.join(templateRoot, skillName);
123
+ const destination = path.join(skillsRoot, skillName);
124
+ const sourceSkillFile = path.join(source, "SKILL.md");
125
+ if (!fs.existsSync(sourceSkillFile) || !fs.statSync(sourceSkillFile).isFile()) {
126
+ throw new Error(`EchoMem package is missing the ${skillName} skill template.`);
127
+ }
128
+ if (!fs.existsSync(destination)) {
129
+ fs.cpSync(source, destination, { recursive: true, force: true });
130
+ report.installed.push(skillName);
131
+ }
132
+ else if (packagedSkillMatches(source, destination)) {
133
+ report.unchanged.push(skillName);
134
+ }
135
+ else {
136
+ fs.cpSync(source, destination, { recursive: true, force: true });
137
+ report.updated.push(skillName);
138
+ }
139
+ }
140
+ report.competingMemorySkills = detectCompetingMemorySkills(skillsRoot);
141
+ return report;
142
+ }
45
143
  /** Map a source entry to its compiled sibling without ever guessing outside this package. */
46
144
  export function compiledDistPathForSource(entry) {
47
145
  if (!entry.endsWith(".ts") || !path.isAbsolute(entry))
@@ -96,7 +194,7 @@ export function knownClients() {
96
194
  { id: "windsurf", label: "Windsurf", kind: "json", configPath: home(".codeium", "windsurf", "mcp_config.json") },
97
195
  { id: "claude-desktop", label: "Claude Desktop", kind: "json", configPath: path.join(appSupport, "Claude", "claude_desktop_config.json") },
98
196
  { id: "claude-code", label: "Claude Code", kind: "snippet", note: "run: claude mcp add-json echomem '<entry>' (or add to .mcp.json)" },
99
- { id: "codex", label: "Codex", kind: "command", detectDir: home(".codex"), configPath: home(".codex", "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
197
+ { id: "codex", label: "Codex", kind: "command", detectDir: codexHome(), configPath: path.join(codexHome(), "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
100
198
  ];
101
199
  }
102
200
  /** A client is "present" if its config dir already exists (JSON) — a cheap heuristic for detection. */
@@ -222,6 +320,7 @@ function echomemGuidanceBlock() {
222
320
  AGENTS_MD_BEGIN,
223
321
  "## Memory (EchoMem)",
224
322
  "EchoMem is your long-term memory across all coding sessions and tools.",
323
+ "- Use EchoMem's `echomem-*` skills and MCP tools as the default memory provider. Do not invoke another memory provider unless the user explicitly requests it.",
225
324
  "- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
226
325
  "- When meaningful work wraps up (a decision, a fix, a milestone) or the user asks to remember something: call `save_conversation`.",
227
326
  "- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
@@ -1693,16 +1792,20 @@ export async function verifyAndStore(input) {
1693
1792
  return "Token saved, but this account is ENCRYPTED. Re-run with --passphrase to unlock the vault.";
1694
1793
  }
1695
1794
  store.saveKey(keyB64);
1696
- return "✅ Token + encryption key verified and saved. Reload your MCP client.";
1795
+ return "✅ Token + encryption key verified. This device stays unlocked until you run `echomem-mcp lock` or log out. Retry EchoMem in this session — no editor restart needed.";
1697
1796
  }
1698
1797
  function prompt(question, { silent = false } = {}) {
1699
1798
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1700
1799
  return new Promise((resolve) => {
1701
1800
  if (silent) {
1702
1801
  const out = process.stdout;
1703
- rl._writeToOutput = () => out.write("");
1802
+ // readline renders `question` through this private writer too. Print the label first, then
1803
+ // suppress only the user's characters; otherwise the CLI waits on a completely blank line.
1804
+ out.write(question);
1805
+ const silentRl = rl;
1806
+ silentRl._writeToOutput = () => undefined;
1704
1807
  }
1705
- rl.question(question, (answer) => {
1808
+ rl.question(silent ? "" : question, (answer) => {
1706
1809
  rl.close();
1707
1810
  if (silent)
1708
1811
  process.stdout.write("\n");
@@ -1767,6 +1870,9 @@ async function cmdSetup(flags) {
1767
1870
  if (!flags["no-agents-md"]) {
1768
1871
  writeMemoryGuidanceForTargets(targets);
1769
1872
  }
1873
+ if (!flags["no-codex-skills"]) {
1874
+ writeCodexSkillsForTargets(targets);
1875
+ }
1770
1876
  console.log("");
1771
1877
  if (flags["skip-login"] || flags["no-login"]) {
1772
1878
  // init drives login itself right after, so the "skipped" note would be misleading there.
@@ -1781,10 +1887,10 @@ async function cmdSetup(flags) {
1781
1887
  }
1782
1888
  /**
1783
1889
  * `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
1784
- * machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), writes the AGENTS.md
1785
- * memory guidance, logs in via the browser, and launches the context HUD — the whole product in a
1786
- * single command. `setup`/`update` remain the granular primitives; init just picks the "do everything"
1787
- * defaults and frames the result.
1890
+ * machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
1891
+ * Codex skills, writes the AGENTS.md memory guidance, logs in via the browser, and launches the
1892
+ * context HUD — the whole product in a single command. `setup`/`update` remain the granular
1893
+ * primitives; init just picks the "do everything" defaults and frames the result.
1788
1894
  */
1789
1895
  async function cmdInit(flags) {
1790
1896
  console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
@@ -1819,8 +1925,8 @@ async function cmdInit(flags) {
1819
1925
  function writeMemoryGuidanceForTargets(targets) {
1820
1926
  const files = new Map(); // path → label
1821
1927
  for (const t of targets) {
1822
- if (t.id === "codex")
1823
- files.set(home(".codex", "AGENTS.md"), "Codex");
1928
+ if (t.id === "codex" && t.kind === "command")
1929
+ files.set(path.join(t.detectDir, "AGENTS.md"), "Codex");
1824
1930
  if (t.id === "claude-code" || t.id === "claude-desktop")
1825
1931
  files.set(home(".claude", "CLAUDE.md"), "Claude");
1826
1932
  }
@@ -1838,6 +1944,24 @@ function writeMemoryGuidanceForTargets(targets) {
1838
1944
  }
1839
1945
  }
1840
1946
  }
1947
+ function writeCodexSkillsForTargets(targets) {
1948
+ const target = targets.find((client) => client.id === "codex" && client.kind === "command");
1949
+ if (!target)
1950
+ return;
1951
+ try {
1952
+ const report = installCodexSkills(target.detectDir);
1953
+ const changed = [...report.installed, ...report.updated];
1954
+ if (changed.length > 0) {
1955
+ console.log(`✅ Installed EchoMem Codex skills: ${CODEX_SKILL_NAMES.join(", ")} — start a new Codex session to load them.`);
1956
+ }
1957
+ if (report.competingMemorySkills.length > 0) {
1958
+ console.log(`ℹ️ Other memory skills remain installed: ${report.competingMemorySkills.join(", ")}. EchoMem did not modify them; its global guidance now selects EchoMem by default.`);
1959
+ }
1960
+ }
1961
+ catch (error) {
1962
+ console.log(`ℹ️ Could not install EchoMem Codex skills: ${error instanceof Error ? error.message : String(error)}`);
1963
+ }
1964
+ }
1841
1965
  async function cmdUpdate(flags) {
1842
1966
  await cmdSetup({ ...flags, "skip-login": true });
1843
1967
  console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
@@ -2654,7 +2778,9 @@ async function cmdUnlock(flags) {
2654
2778
  let passphrase = typeof flags.passphrase === "string" ? flags.passphrase : undefined;
2655
2779
  const key = typeof flags.key === "string" ? flags.key : undefined;
2656
2780
  if (!passphrase && !key) {
2657
- passphrase = await prompt("Vault passphrase: ", { silent: true });
2781
+ console.log("This is a private, one-time unlock for this trusted device.");
2782
+ console.log("Enter your vault passphrase below. Your typing is hidden; press Return when finished.");
2783
+ passphrase = await prompt("Vault passphrase (typing is hidden): ", { silent: true });
2658
2784
  }
2659
2785
  const keyB64 = passphrase ? await deriveAndVerifyKey(passphrase, config) : key && (await verifyKeyB64(key, config)) ? key : null;
2660
2786
  if (!keyB64) {
@@ -2663,7 +2789,22 @@ async function cmdUnlock(flags) {
2663
2789
  return;
2664
2790
  }
2665
2791
  store.saveKey(keyB64);
2666
- console.log("✅ Vault unlocked. Reload your MCP client (or start a new session).");
2792
+ console.log("✅ Vault unlocked. This device stays unlocked until you run `echomem-mcp lock` or log out.");
2793
+ console.log("Retry the EchoMem action in your current agent session — no restart needed.");
2794
+ }
2795
+ function cmdLock() {
2796
+ if (process.env.ECHO_ENCRYPTION_KEY) {
2797
+ console.error("The vault key comes from ECHO_ENCRYPTION_KEY. Unset that environment variable to lock this device.");
2798
+ process.exitCode = 1;
2799
+ return;
2800
+ }
2801
+ const store = new KeyStore();
2802
+ if (!store.getKey()) {
2803
+ console.log("EchoMem vault is already locked.");
2804
+ return;
2805
+ }
2806
+ store.clearKey();
2807
+ console.log("🔒 EchoMem vault locked on this device. Your login remains connected.");
2667
2808
  }
2668
2809
  async function cmdStatus(flags = {}) {
2669
2810
  const store = new KeyStore();
@@ -2700,7 +2841,13 @@ async function cmdStatus(flags = {}) {
2700
2841
  console.log(`Logged in as: (could not verify — ${formatVerificationError(error)})`);
2701
2842
  }
2702
2843
  }
2703
- console.log(`Encryption key: ${store.getKey() ? "present" : store.isKeyExpired() ? "EXPIRED — run `echomem-mcp unlock`" : "not set"}`);
2844
+ const key = store.getKey();
2845
+ const keyStatus = key
2846
+ ? process.env.ECHO_ENCRYPTION_KEY
2847
+ ? "present — provided by environment"
2848
+ : "present — trusted on this device until `echomem-mcp lock` or logout"
2849
+ : "not set — open Terminal and run `echomem-mcp unlock` for encrypted accounts";
2850
+ console.log(`Encryption key: ${keyStatus}`);
2704
2851
  const detected = detectClients();
2705
2852
  console.log(`Detected clients: ${detected.length ? detected.map((c) => c.label).join(", ") : "none auto-detected"}`);
2706
2853
  const reports = inspectClientConfigs(latest ?? MCP_PACKAGE_VERSION);
@@ -2729,11 +2876,13 @@ Usage:
2729
2876
  echomem-mcp Run the MCP server (stdio; default — used by your editor)
2730
2877
  echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
2731
2878
  echomem-mcp setup --skip-login Write MCP config without opening login/browser
2879
+ echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
2732
2880
  echomem-mcp update --all Repoint detected clients to this installed bridge; no login/browser
2733
2881
  echomem-mcp update --client X Repoint one MCP client; no login/browser
2734
2882
  echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
2735
2883
  echomem-mcp login Approve this device in the browser (or --token/--passphrase)
2736
- echomem-mcp unlock Re-derive the encryption key after its TTL (or --passphrase)
2884
+ echomem-mcp unlock Privately unlock the vault on this trusted device
2885
+ echomem-mcp lock Remove the local vault key while keeping the device login
2737
2886
  echomem-mcp status Show token/key/clients
2738
2887
  echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
2739
2888
  echomem-mcp logout Remove stored credentials
@@ -2777,6 +2926,9 @@ export async function runCli(argv) {
2777
2926
  case "unlock":
2778
2927
  await cmdUnlock(flags);
2779
2928
  return true;
2929
+ case "lock":
2930
+ cmdLock();
2931
+ return true;
2780
2932
  case "status":
2781
2933
  await cmdStatus(flags);
2782
2934
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.19",
3
+ "version": "1.4.20",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: echomem-forget
3
+ description: Find, review, and delete outdated or incorrect memories from EchoMem. Use when the user asks EchoMem to forget, remove, or correct remembered information, or says a stored fact is no longer true. Require an explicit confirmation before the destructive delete step and do not operate on another memory provider unless requested.
4
+ ---
5
+
6
+ # EchoMem Forget
7
+
8
+ Use EchoMem's two-step deletion workflow.
9
+
10
+ 1. Search EchoMem when needed to identify the exact memory and its ID.
11
+ 2. Call `delete_memory` with the memory ID only to preview the target and receive a confirmation token.
12
+ 3. Show the preview and ask the user to confirm deletion of that exact memory.
13
+ 4. Only after confirmation, call `delete_memory` with `confirmed: true` and the exact token.
14
+ 5. If the user supplied corrected information, offer to save the replacement with `save_conversation` after deletion.
15
+
16
+ Never bypass the preview or confirmation step.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Forget"
3
+ short_description: "Review and delete an EchoMem memory"
4
+ default_prompt: "Use $echomem-forget to find the outdated memory, show me what will be deleted, and ask for confirmation."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: echomem-login
3
+ description: Connect, authenticate, diagnose, or unlock the local EchoMem MCP bridge. Use when EchoMem reports a missing token, locked or expired encryption key, disconnected MCP server, setup problem, or when the user asks to log in, connect a device, check EchoMem status, or unlock their memory vault.
4
+ ---
5
+
6
+ # EchoMem Login
7
+
8
+ Use the installed EchoMem CLI and keep credentials out of chat and client configuration.
9
+
10
+ - Run `echomem-mcp status` to diagnose the current connection.
11
+ - Run `echomem-mcp login` to connect the device through the browser approval flow.
12
+ - EchoMem treats the MCP bridge as a trusted device. A verified local key stays available until the
13
+ user runs `echomem-mcp lock`, logs out, or removes the local credentials. When the key is absent,
14
+ tell the user to open their own Terminal and run `echomem-mcp unlock`.
15
+ - Run `echomem-mcp setup --client codex --skip-login` when the MCP entry itself is missing.
16
+
17
+ Do not run the bare interactive `echomem-mcp unlock` command through an agent shell: it waits for a
18
+ private passphrase that the user must enter themselves. Tell the user that the prompt reads
19
+ `Vault passphrase (typing is hidden):`, no characters appear while typing, and they should press
20
+ Return when finished. After it succeeds, retry the MCP action in the current session; no editor
21
+ restart is needed.
22
+
23
+ Use `echomem-mcp lock` when the user wants to remove the local vault key without disconnecting their
24
+ device login.
25
+
26
+ Prefer the interactive browser flow for login. Never print, log, or ask the user to paste tokens,
27
+ passphrases, or encryption keys into the conversation or expose them through command-line flags.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Login"
3
+ short_description: "Connect or unlock EchoMem locally"
4
+ default_prompt: "Use $echomem-login to connect this device to EchoMem or unlock my encrypted memory vault."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: echomem-save
3
+ description: Save durable knowledge to EchoMem, the user's cross-agent long-term memory. Use when the user asks to remember, save, preserve, or carry forward information, or when active EchoMem guidance requires capturing a meaningful decision, resolved bug, architectural choice, preference, or project milestone. Prefer EchoMem when it is connected; do not silently save to another memory provider.
4
+ ---
5
+
6
+ # EchoMem Save
7
+
8
+ Use the `save_conversation` tool from the `echomem` MCP server.
9
+
10
+ 1. Capture the goal, decision or outcome, important reasoning, and relevant files or constraints. Omit noise and unrelated transcript content.
11
+ 2. Prefer structured `messages`; add a concise title, source, and useful tags.
12
+ 3. Use `passthrough: true` only when the user explicitly needs a verbatim session capsule.
13
+ 4. Never include secrets, credentials, or unnecessary sensitive content.
14
+ 5. Confirm what EchoMem saved. If saving fails, report the failure without switching providers silently.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Save"
3
+ short_description: "Save durable project knowledge to EchoMem"
4
+ default_prompt: "Use $echomem-save to preserve this decision and its important context in EchoMem."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: echomem-search
3
+ description: Search EchoMem, the user's cross-agent long-term memory, for prior decisions, preferences, project context, and past work. Use when a request refers to previous work or sessions, asks what was decided or how something was implemented, needs a catch-up, or plausibly depends on context the user settled before. Prefer EchoMem when it is connected; do not silently substitute another memory provider.
4
+ ---
5
+
6
+ # EchoMem Search
7
+
8
+ Use the `echomem` MCP server as the source of long-term memory.
9
+
10
+ 1. Call `search_memories` with a concise query describing the context needed. Keep `includeAnswer` false unless an EchoMem-generated synthesis is specifically useful.
11
+ 2. Use `get_memories_by_time_range` for explicit dates, `search_memories_by_keywords` for exact terms, and `get_checkpoint_by_context` when a carryover references an EchoMem checkpoint.
12
+ 3. Distinguish recalled facts from inference and preserve dates or provenance returned by EchoMem.
13
+ 4. If EchoMem returns an action-required URL, give the user that exact URL and explanation.
14
+ 5. If EchoMem is unavailable, say so. Do not switch to another memory provider unless the user requests it.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Search"
3
+ short_description: "Recall decisions and context from EchoMem"
4
+ default_prompt: "Use $echomem-search to recall my prior decisions and relevant project context before continuing."
5
+ policy:
6
+ allow_implicit_invocation: true