@echomem/mcp 1.4.2 → 1.4.3

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/dist/hud/cli.js CHANGED
File without changes
package/dist/hud/web.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { MCP_PACKAGE_VERSION } from "../package-metadata.js";
1
2
  export const HUD_HTML = String.raw `<!doctype html>
2
3
  <html lang="en">
3
4
  <head>
@@ -193,6 +194,7 @@ export const HUD_HTML = String.raw `<!doctype html>
193
194
  const history = [];
194
195
  let lastSource = '';
195
196
  let prevTurnState = null;
197
+ const hudVersion = 'v${MCP_PACKAGE_VERSION}';
196
198
  updateBubbleSkins();
197
199
  if ('ResizeObserver' in window) {
198
200
  const observer = new ResizeObserver(updateBubbleSkins);
@@ -261,7 +263,7 @@ export const HUD_HTML = String.raw `<!doctype html>
261
263
  if (deltaEl) deltaEl.style.display = 'none';
262
264
  prevTurnState = null;
263
265
  main.textContent = 'No active session yet';
264
- client.textContent = '';
266
+ client.textContent = 'EchoMem HUD · ' + hudVersion;
265
267
  if (summary) summary.textContent = 'Start working in Codex or Claude and I’ll check the context here.';
266
268
  if (fullness) fullness.textContent = '';
267
269
  if (meterfill) meterfill.style.width = '0';
@@ -285,7 +287,7 @@ export const HUD_HTML = String.raw `<!doctype html>
285
287
  hud.classList.remove('s-green', 's-amber', 's-red');
286
288
  hud.classList.add('s-' + (score.color || 'green'));
287
289
  main.textContent = stateWord(score.color) + ' · ' + fmt(score.ctTokens) + ' ctx';
288
- client.textContent = label(score.client);
290
+ client.textContent = label(score.client) + ' · ' + hudVersion;
289
291
  if (summary) summary.textContent = summaryLine(score.color);
290
292
  const sat = typeof score.saturationPct === 'number' ? score.saturationPct
291
293
  : (score.modelContextWindow ? Math.round(100 * (score.ctTokens || 0) / score.modelContextWindow) : 0);
package/dist/index.js CHANGED
File without changes
package/dist/setup.js CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import http from "node:http";
16
16
  import { randomUUID } from "node:crypto";
17
- import { spawn } from "node:child_process";
17
+ import { execFileSync, spawn } from "node:child_process";
18
18
  import { Worker } from "node:worker_threads";
19
19
  import fs from "node:fs";
20
20
  import os from "node:os";
@@ -30,7 +30,7 @@ import { syncCodexUsage } from "./codex-sync.js";
30
30
  import { renderSetupPage } from "./setup-page.js";
31
31
  import { repoLabel } from "./forensics.js";
32
32
  import { installHooks } from "./hud/hooks.js";
33
- import { MCP_PACKAGE_LABEL, MCP_PACKAGE_VERSION, MCP_UPDATE_COMMAND } from "./package-metadata.js";
33
+ import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_COMMAND } from "./package-metadata.js";
34
34
  // The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
35
35
  // served by this localhost bridge, where local logs and processed/unprocessed counts never leave the
36
36
  // device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
@@ -142,6 +142,283 @@ export function writeJsonClientConfig(configPath, entry) {
142
142
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
143
143
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
144
144
  }
145
+ export function writeClaudeCodeConfig(entry) {
146
+ try {
147
+ execFileSync("claude", ["mcp", "add-json", "echomem", JSON.stringify(entry)], {
148
+ encoding: "utf8",
149
+ stdio: ["ignore", "pipe", "pipe"],
150
+ timeout: 10000,
151
+ });
152
+ return "wrote";
153
+ }
154
+ catch {
155
+ return "unavailable";
156
+ }
157
+ }
158
+ function readJsonClientEntry(configPath) {
159
+ try {
160
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
161
+ const servers = config.mcpServers;
162
+ if (!servers || typeof servers !== "object" || Array.isArray(servers))
163
+ return null;
164
+ const entry = servers.echomem;
165
+ return entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ function readCodexEntry(configPath) {
172
+ let content = "";
173
+ try {
174
+ content = fs.readFileSync(configPath, "utf8");
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ const lines = content.split("\n");
180
+ const start = lines.findIndex((line) => /^\s*\[mcp_servers\.echomem\]\s*$/.test(line));
181
+ if (start < 0)
182
+ return null;
183
+ let end = start + 1;
184
+ while (end < lines.length && !/^\s*\[/.test(lines[end]))
185
+ end++;
186
+ const block = lines.slice(start, end);
187
+ const command = parseTomlString(block.find((line) => /^\s*command\s*=/.test(line)));
188
+ const args = parseTomlStringArray(block.find((line) => /^\s*args\s*=/.test(line)));
189
+ return command ? { command, args } : null;
190
+ }
191
+ function parseTomlString(line) {
192
+ if (!line)
193
+ return undefined;
194
+ const match = line.match(/=\s*(".*")\s*$/);
195
+ if (!match)
196
+ return undefined;
197
+ try {
198
+ const value = JSON.parse(match[1]);
199
+ return typeof value === "string" ? value : undefined;
200
+ }
201
+ catch {
202
+ return undefined;
203
+ }
204
+ }
205
+ function parseTomlStringArray(line) {
206
+ if (!line)
207
+ return [];
208
+ const match = line.match(/=\s*(\[.*\])\s*$/);
209
+ if (!match)
210
+ return [];
211
+ try {
212
+ const value = JSON.parse(match[1]);
213
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
214
+ }
215
+ catch {
216
+ return [];
217
+ }
218
+ }
219
+ function entryArgs(entry) {
220
+ return Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
221
+ }
222
+ function entryCommand(entry) {
223
+ return typeof entry.command === "string" && entry.command.trim() ? entry.command.trim() : undefined;
224
+ }
225
+ function entryTarget(entry) {
226
+ const command = entryCommand(entry) ?? "(missing command)";
227
+ const args = entryArgs(entry);
228
+ return [command, ...args].join(" ");
229
+ }
230
+ function packageVersionFromPath(entryPath) {
231
+ let current = entryPath;
232
+ try {
233
+ current = fs.realpathSync(entryPath);
234
+ }
235
+ catch {
236
+ /* keep the original path; it may still be inside an existing package dir */
237
+ }
238
+ try {
239
+ if (fs.existsSync(current) && fs.statSync(current).isFile())
240
+ current = path.dirname(current);
241
+ }
242
+ catch {
243
+ current = path.dirname(current);
244
+ }
245
+ for (let i = 0; i < 12; i += 1) {
246
+ const packagePath = path.join(current, "package.json");
247
+ try {
248
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
249
+ if (pkg.name === MCP_PACKAGE_NAME && typeof pkg.version === "string") {
250
+ return { version: pkg.version, packagePath };
251
+ }
252
+ }
253
+ catch {
254
+ /* keep walking */
255
+ }
256
+ const parent = path.dirname(current);
257
+ if (parent === current)
258
+ break;
259
+ current = parent;
260
+ }
261
+ return null;
262
+ }
263
+ export function resolveServerEntryVersion(entry) {
264
+ const command = entryCommand(entry);
265
+ const args = entryArgs(entry);
266
+ const npxPackageArg = args.find((arg) => arg === MCP_PACKAGE_NAME || arg.startsWith(`${MCP_PACKAGE_NAME}@`));
267
+ if (command && path.basename(command).replace(/\.(cmd|exe)$/i, "") === "npx" && npxPackageArg) {
268
+ const suffix = npxPackageArg.slice(MCP_PACKAGE_NAME.length);
269
+ if (suffix === "@latest")
270
+ return { runtime: "latest" };
271
+ if (suffix.startsWith("@") && /^\d+\.\d+\.\d+/.test(suffix.slice(1)))
272
+ return { version: suffix.slice(1) };
273
+ return { runtime: "dynamic" };
274
+ }
275
+ const candidates = [...args, command].filter((value) => typeof value === "string");
276
+ for (const candidate of candidates) {
277
+ if (!candidate.includes(MCP_PACKAGE_NAME) && !candidate.includes(`${path.sep}echomem-mcp`))
278
+ continue;
279
+ const resolved = packageVersionFromPath(candidate);
280
+ if (resolved)
281
+ return resolved;
282
+ }
283
+ return {};
284
+ }
285
+ function compareSemver(a, b) {
286
+ const av = a.split(/[.-]/).map((part) => Number(part));
287
+ const bv = b.split(/[.-]/).map((part) => Number(part));
288
+ for (let i = 0; i < Math.max(av.length, bv.length, 3); i += 1) {
289
+ const ai = Number.isFinite(av[i]) ? av[i] : 0;
290
+ const bi = Number.isFinite(bv[i]) ? bv[i] : 0;
291
+ if (ai !== bi)
292
+ return ai > bi ? 1 : -1;
293
+ }
294
+ return 0;
295
+ }
296
+ function versionState(version, desiredVersion) {
297
+ if (!version || !desiredVersion)
298
+ return "unknown";
299
+ const cmp = compareSemver(version, desiredVersion);
300
+ if (cmp < 0)
301
+ return "stale";
302
+ if (cmp > 0)
303
+ return "newer";
304
+ return "ok";
305
+ }
306
+ function readLatestPublishedVersion() {
307
+ try {
308
+ const raw = execFileSync("npm", ["view", MCP_PACKAGE_NAME, "version", "--silent"], {
309
+ encoding: "utf8",
310
+ stdio: ["ignore", "pipe", "ignore"],
311
+ timeout: 3500,
312
+ }).trim();
313
+ return /^\d+\.\d+\.\d+/.test(raw) ? raw : undefined;
314
+ }
315
+ catch {
316
+ return undefined;
317
+ }
318
+ }
319
+ function inspectClientConfig(client, desiredVersion) {
320
+ if (client.kind === "snippet")
321
+ return inspectClaudeCodeConfig(client, desiredVersion);
322
+ if (client.kind === "json" && !fs.existsSync(path.dirname(client.configPath)))
323
+ return null;
324
+ if (client.kind === "command" && !fs.existsSync(client.detectDir))
325
+ return null;
326
+ const entry = client.kind === "json" ? readJsonClientEntry(client.configPath) : readCodexEntry(client.configPath);
327
+ if (!entry) {
328
+ return {
329
+ id: client.id,
330
+ label: client.label,
331
+ configured: false,
332
+ detail: `no EchoMem MCP entry in ${client.configPath}`,
333
+ state: "missing",
334
+ command: `${MCP_UPDATE_COMMAND} --client ${client.id}`,
335
+ };
336
+ }
337
+ const resolved = resolveServerEntryVersion(entry);
338
+ const state = resolved.runtime === "latest" ? "ok" : versionState(resolved.version, desiredVersion);
339
+ return {
340
+ id: client.id,
341
+ label: client.label,
342
+ configured: true,
343
+ detail: entryTarget(entry),
344
+ version: resolved.version,
345
+ packagePath: resolved.packagePath,
346
+ runtime: resolved.runtime,
347
+ state,
348
+ command: state === "stale" || state === "missing" ? `${MCP_UPDATE_COMMAND} --client ${client.id}` : undefined,
349
+ };
350
+ }
351
+ function inspectClaudeCodeConfig(client, desiredVersion) {
352
+ if (!fs.existsSync(home(".claude")))
353
+ return null;
354
+ try {
355
+ const output = execFileSync("claude", ["mcp", "list"], {
356
+ encoding: "utf8",
357
+ stdio: ["ignore", "pipe", "ignore"],
358
+ timeout: 3000,
359
+ });
360
+ const line = output.split("\n").find((item) => item.toLowerCase().includes("echomem"));
361
+ if (!line) {
362
+ return {
363
+ id: client.id,
364
+ label: client.label,
365
+ configured: false,
366
+ detail: "Claude Code reports no EchoMem MCP server",
367
+ state: "missing",
368
+ command: `${MCP_UPDATE_COMMAND} --client ${client.id}`,
369
+ };
370
+ }
371
+ const match = line.match(new RegExp(`${MCP_PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@(\\d+\\.\\d+\\.\\d+)`));
372
+ const version = match?.[1];
373
+ return {
374
+ id: client.id,
375
+ label: client.label,
376
+ configured: true,
377
+ detail: line.trim(),
378
+ version,
379
+ state: versionState(version, desiredVersion),
380
+ command: version && versionState(version, desiredVersion) === "stale" ? `${MCP_UPDATE_COMMAND} --client ${client.id}` : undefined,
381
+ };
382
+ }
383
+ catch {
384
+ return {
385
+ id: client.id,
386
+ label: client.label,
387
+ configured: false,
388
+ detail: "Claude Code CLI not available for `claude mcp list`; add EchoMem from Claude Code manually",
389
+ state: "unknown",
390
+ };
391
+ }
392
+ }
393
+ function inspectClientConfigs(desiredVersion) {
394
+ return knownClients()
395
+ .map((client) => inspectClientConfig(client, desiredVersion))
396
+ .filter((report) => report !== null);
397
+ }
398
+ function formatClientConfigReport(report) {
399
+ const lines = [];
400
+ const status = report.state === "ok"
401
+ ? "ok"
402
+ : report.state === "stale"
403
+ ? "stale"
404
+ : report.state === "newer"
405
+ ? "newer than this status command"
406
+ : report.state === "missing"
407
+ ? "missing"
408
+ : "unknown";
409
+ const version = report.runtime === "latest"
410
+ ? "latest at launch"
411
+ : report.runtime === "dynamic"
412
+ ? "dynamic npm resolution"
413
+ : report.version
414
+ ? `${MCP_PACKAGE_NAME}@${report.version}`
415
+ : "version unknown";
416
+ lines.push(` ${report.label}: ${report.configured ? "configured" : "not configured"} (${status}; ${version})`);
417
+ lines.push(` ${report.detail}`);
418
+ if (report.command)
419
+ lines.push(` Update: ${report.command}`);
420
+ return lines;
421
+ }
145
422
  // ---------------------------------------------------------------------------
146
423
  // Browser + localhost callback
147
424
  // ---------------------------------------------------------------------------
@@ -788,7 +1065,13 @@ async function cmdSetup(flags) {
788
1065
  console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
789
1066
  }
790
1067
  else {
791
- console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
1068
+ const result = c.id === "claude-code" ? writeClaudeCodeConfig(entry) : "unavailable";
1069
+ if (result === "wrote") {
1070
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
1071
+ }
1072
+ else {
1073
+ console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
1074
+ }
792
1075
  }
793
1076
  }
794
1077
  }
@@ -1381,10 +1664,17 @@ async function cmdUnlock(flags) {
1381
1664
  store.saveKey(keyB64);
1382
1665
  console.log("✅ Vault unlocked. Reload your MCP client (or start a new session).");
1383
1666
  }
1384
- async function cmdStatus() {
1667
+ async function cmdStatus(flags = {}) {
1385
1668
  const store = new KeyStore();
1386
1669
  const token = store.getToken();
1387
1670
  console.log(`EchoMem MCP: ${MCP_PACKAGE_LABEL}`);
1671
+ const latest = flags["no-network"] ? undefined : readLatestPublishedVersion();
1672
+ if (latest) {
1673
+ console.log(`Published latest: ${MCP_PACKAGE_NAME}@${latest}`);
1674
+ if (compareSemver(MCP_PACKAGE_VERSION, latest) < 0) {
1675
+ console.log(`This status command is older than latest. Update client configs with: ${MCP_UPDATE_COMMAND}`);
1676
+ }
1677
+ }
1388
1678
  console.log(`Credentials file: ${store.path()}`);
1389
1679
  console.log(`API token: ${token ? "present" : "MISSING — run `echomem-mcp login`"}`);
1390
1680
  if (token) {
@@ -1408,6 +1698,14 @@ async function cmdStatus() {
1408
1698
  console.log(`Encryption key: ${store.getKey() ? "present" : store.isKeyExpired() ? "EXPIRED — run `echomem-mcp unlock`" : "not set"}`);
1409
1699
  const detected = detectClients();
1410
1700
  console.log(`Detected clients: ${detected.length ? detected.map((c) => c.label).join(", ") : "none auto-detected"}`);
1701
+ const reports = inspectClientConfigs(latest ?? MCP_PACKAGE_VERSION);
1702
+ if (reports.length) {
1703
+ console.log("Client MCP configs:");
1704
+ for (const report of reports) {
1705
+ for (const line of formatClientConfigReport(report))
1706
+ console.log(line);
1707
+ }
1708
+ }
1411
1709
  }
1412
1710
  function cmdLogout() {
1413
1711
  const store = new KeyStore();
@@ -1430,6 +1728,7 @@ Usage:
1430
1728
  echomem-mcp login Approve this device in the browser (or --token/--passphrase)
1431
1729
  echomem-mcp unlock Re-derive the encryption key after its TTL (or --passphrase)
1432
1730
  echomem-mcp status Show token/key/clients
1731
+ echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
1433
1732
  echomem-mcp logout Remove stored credentials
1434
1733
  echomem-mcp report [--json] Your AI coding memory audit (local, no login, $0)
1435
1734
  echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
@@ -1468,7 +1767,10 @@ export async function runCli(argv) {
1468
1767
  await cmdUnlock(flags);
1469
1768
  return true;
1470
1769
  case "status":
1471
- await cmdStatus();
1770
+ await cmdStatus(flags);
1771
+ return true;
1772
+ case "doctor":
1773
+ await cmdStatus(flags);
1472
1774
  return true;
1473
1775
  case "logout":
1474
1776
  cmdLogout();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "EchoMem Cloud-First MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",