@echomem/mcp 1.4.42 → 1.4.43

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/index.js CHANGED
@@ -613,6 +613,8 @@ function toolEventName(canonicalName, status) {
613
613
  class EchoMemApiClient {
614
614
  store;
615
615
  axios;
616
+ activeToken;
617
+ accountGeneration = 0;
616
618
  boundSourceSession = null;
617
619
  requestSourceSession = new AsyncLocalStorage();
618
620
  sourceSessionsByCanonicalKey = new Map();
@@ -623,6 +625,7 @@ class EchoMemApiClient {
623
625
  deleteConfirmations = new Map();
624
626
  constructor(store) {
625
627
  this.store = store;
628
+ this.activeToken = store.getToken();
626
629
  this.axios = axios.create({
627
630
  baseURL: ECHO_API_BASE_URL.replace(/\/$/, ""),
628
631
  headers: {
@@ -632,7 +635,7 @@ class EchoMemApiClient {
632
635
  // Read the token fresh on EVERY request (not baked in at construction) so a `login` that runs
633
636
  // after the editor already launched the bridge is picked up on the next call — no restart.
634
637
  this.axios.interceptors.request.use((config) => {
635
- const token = this.store.getToken();
638
+ const token = this.synchronizeAccountContext();
636
639
  if (token)
637
640
  config.headers.set("Authorization", `Bearer ${token}`);
638
641
  const sourceSession = this.getBoundSourceSession();
@@ -642,21 +645,46 @@ class EchoMemApiClient {
642
645
  return config;
643
646
  });
644
647
  }
648
+ /**
649
+ * Account-scoped state must move atomically with the credential file. The token is intentionally
650
+ * read fresh, but encryption, identity, source-session, and confirmation caches belong to the
651
+ * token that populated them and cannot survive an in-process account switch.
652
+ */
653
+ synchronizeAccountContext() {
654
+ const token = this.store.getToken();
655
+ if (token === this.activeToken)
656
+ return token;
657
+ this.activeToken = token;
658
+ this.accountGeneration += 1;
659
+ this.encConfigPromise = null;
660
+ this.whoamiCache = null;
661
+ this.boundSourceSession = null;
662
+ this.sourceSessionsByCanonicalKey.clear();
663
+ this.deleteConfirmations.clear();
664
+ return token;
665
+ }
666
+ /** Lets the MCP server invalidate its account-scoped tool-description caches too. */
667
+ refreshAccountContext() {
668
+ this.synchronizeAccountContext();
669
+ return this.accountGeneration;
670
+ }
645
671
  /** Whether a usable API token currently exists (read fresh from the keystore each call). */
646
672
  hasToken() {
647
- return !!this.store.getToken();
673
+ return !!this.synchronizeAccountContext();
648
674
  }
649
675
  /** The per-process session id — the join key telemetry shares with grouped saves. */
650
676
  getSessionId() {
651
677
  return this.sessionId;
652
678
  }
653
679
  getBoundSourceSession() {
680
+ this.synchronizeAccountContext();
654
681
  return this.requestSourceSession.getStore() ?? this.boundSourceSession;
655
682
  }
656
683
  async withSourceSession(sourceSession, operation) {
657
684
  return sourceSession ? this.requestSourceSession.run(sourceSession, operation) : operation();
658
685
  }
659
686
  async bindSourceSession(verified, persistForBridge = true) {
687
+ this.synchronizeAccountContext();
660
688
  const cached = this.sourceSessionsByCanonicalKey.get(verified.canonicalKey);
661
689
  if (cached) {
662
690
  if (persistForBridge)
@@ -739,16 +767,23 @@ class EchoMemApiClient {
739
767
  return undefined;
740
768
  }
741
769
  }
742
- /** Encryption config for the account, fetched once and cached. Failures are not cached. */
770
+ /** Encryption config for the current account, cached only for the lifetime of its token. */
743
771
  async getEncryptionConfig() {
772
+ const generation = this.refreshAccountContext();
744
773
  if (!this.encConfigPromise) {
745
- this.encConfigPromise = fetchEncryptionConfig(this.axios).catch((error) => {
746
- this.encConfigPromise = null; // allow retry next call
774
+ const request = fetchEncryptionConfig(this.axios).catch((error) => {
775
+ if (this.encConfigPromise === request)
776
+ this.encConfigPromise = null; // allow retry next call
747
777
  console.error(`[encryption] config fetch failed: ${describeError(error)} — refusing memory access`);
748
778
  throw new EncryptionStatusUnavailableError("EchoMem could not verify the account encryption state");
749
779
  });
780
+ this.encConfigPromise = request;
750
781
  }
751
- return this.encConfigPromise;
782
+ const request = this.encConfigPromise;
783
+ const config = await request;
784
+ if (this.refreshAccountContext() !== generation)
785
+ return this.getEncryptionConfig();
786
+ return config;
752
787
  }
753
788
  /**
754
789
  * Resolve the encryption state for a read/write. For encrypted accounts this REQUIRES a usable
@@ -756,13 +791,19 @@ class EchoMemApiClient {
756
791
  * handing the model ciphertext. For unencrypted accounts it returns `{ enabled: false }`.
757
792
  */
758
793
  async encState() {
794
+ const generation = this.refreshAccountContext();
759
795
  const cfg = await this.getEncryptionConfig();
796
+ if (this.refreshAccountContext() !== generation)
797
+ return this.encState();
760
798
  if (!cfg.enabled)
761
799
  return { enabled: false };
762
800
  const key = this.store.getKey();
763
801
  if (!key)
764
802
  throw new LockedError("EchoMem vault locked");
765
- if (!(await verifyKeyB64(key, cfg))) {
803
+ const isValid = await verifyKeyB64(key, cfg);
804
+ if (this.refreshAccountContext() !== generation)
805
+ return this.encState();
806
+ if (!isValid) {
766
807
  // A file-backed stale key can be removed safely; environment-provided
767
808
  // keys remain under the caller's control and are ignored until corrected.
768
809
  if (!process.env.ECHO_ENCRYPTION_KEY)
@@ -779,16 +820,23 @@ class EchoMemApiClient {
779
820
  }
780
821
  }
781
822
  async whoami() {
823
+ const generation = this.refreshAccountContext();
782
824
  if (!this.whoamiCache) {
783
- this.whoamiCache = this.axios
825
+ const request = this.axios
784
826
  .get("/api/openclaw/v1/whoami", { timeout: 6000 })
785
827
  .then((response) => response.data)
786
828
  .catch((error) => {
787
- this.whoamiCache = null; // don't pin a token-less failure; retry once a token exists
829
+ if (this.whoamiCache === request)
830
+ this.whoamiCache = null;
788
831
  throw error;
789
832
  });
833
+ this.whoamiCache = request;
790
834
  }
791
- return this.whoamiCache;
835
+ const request = this.whoamiCache;
836
+ const identity = await request;
837
+ if (this.refreshAccountContext() !== generation)
838
+ return this.whoami();
839
+ return identity;
792
840
  }
793
841
  async fetchMemoryById(id, enc) {
794
842
  try {
@@ -1255,6 +1303,7 @@ function capWait(pending) {
1255
1303
  class EchoMemMCPServer {
1256
1304
  server;
1257
1305
  client;
1306
+ accountGeneration = 0;
1258
1307
  mapCache = null;
1259
1308
  groupMapCache = null;
1260
1309
  events;
@@ -1327,8 +1376,19 @@ class EchoMemMCPServer {
1327
1376
  platform_source: hostPlatform,
1328
1377
  };
1329
1378
  }
1379
+ refreshAccountContext() {
1380
+ const generation = this.client.refreshAccountContext();
1381
+ if (generation === this.accountGeneration)
1382
+ return;
1383
+ this.accountGeneration = generation;
1384
+ this.mapCache = null;
1385
+ this.groupMapCache = null;
1386
+ this.mapInjected = false;
1387
+ this.groupMapInjected = false;
1388
+ }
1330
1389
  setupToolHandlers() {
1331
1390
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1391
+ this.refreshAccountContext();
1332
1392
  const clientVersion = this.server.getClientVersion();
1333
1393
  this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
1334
1394
  this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
@@ -1360,6 +1420,7 @@ class EchoMemMCPServer {
1360
1420
  return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
1361
1421
  });
1362
1422
  this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1423
+ this.refreshAccountContext();
1363
1424
  const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
1364
1425
  const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
1365
1426
  const canonicalName = recallRoute.canonicalName;
@@ -1090,6 +1090,41 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1090
1090
  function paidRecallPlan(plan) {
1091
1091
  return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
1092
1092
  }
1093
+ var planQuotaCatalog = null;
1094
+ var planQuotaCatalogLastFetchedAt = 0;
1095
+ function quotaLimitsForPlan(plan) {
1096
+ var normalized = String(plan || "free").toLowerCase();
1097
+ if (normalized === "team") normalized = "pro";
1098
+ if (normalized === "enterprise") normalized = "power";
1099
+ var limits = planQuotaCatalog && planQuotaCatalog[normalized];
1100
+ if (!limits || typeof limits !== "object") return null;
1101
+ if (
1102
+ typeof limits.historicalConversationLimit !== "number" ||
1103
+ typeof limits.memoryProcessingInputTokensWeeklyLimit !== "number" ||
1104
+ typeof limits.memorySearchWeeklyLimit !== "number"
1105
+ ) return null;
1106
+ return limits;
1107
+ }
1108
+ function quotaNumber(value) {
1109
+ return number(Math.max(0, Math.floor(value)));
1110
+ }
1111
+ function quotaTokens(value) {
1112
+ var normalized = Math.max(0, Math.floor(value));
1113
+ if (normalized > 0 && normalized % 1000000 === 0) return String(normalized / 1000000) + "M";
1114
+ return normalized > 0 && normalized % 1000 === 0 ? String(normalized / 1000) + "K" : quotaNumber(normalized);
1115
+ }
1116
+ async function refreshPlanQuotaCatalog() {
1117
+ if (Date.now() - planQuotaCatalogLastFetchedAt < 60000) return;
1118
+ try {
1119
+ var response = await fetch("https://echo-mem-chrome.vercel.app/api/public/plan-quotas", { cache: "no-store" });
1120
+ var payload = response.ok ? await response.json() : null;
1121
+ if (!payload || !payload.plans || typeof payload.plans !== "object") return;
1122
+ planQuotaCatalog = payload.plans;
1123
+ planQuotaCatalogLastFetchedAt = Date.now();
1124
+ } catch (_) {
1125
+ // Generic labels remain visible until the managed catalog is available.
1126
+ }
1127
+ }
1093
1128
  function setupPlanPrice(plan) {
1094
1129
  return plan === "power" ? "$100" : "$20";
1095
1130
  }
@@ -1151,7 +1186,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1151
1186
  var statusLabel = canceled ? "Canceled" : (state === "trialing" ? "Trial active" : "Active");
1152
1187
  var lifecycle = paidPlanLifecycleText(plan);
1153
1188
  var quota = billingStatus && billingStatus.historicalConversationQuota;
1154
- var limit = quota && typeof quota.limit === "number" ? quota.limit : (plan === "power" ? 2000 : 500);
1189
+ var limit = quota && typeof quota.limit === "number" ? quota.limit : null;
1155
1190
  var moneyFact = canceled
1156
1191
  ? "No future charge"
1157
1192
  : (state === "trialing" ? setupPlanPrice(plan) + "/month after trial" : setupPlanPrice(plan) + "/month");
@@ -1160,7 +1195,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1160
1195
  '<section class="billingCommitment' + (canceled ? ' is-canceled' : '') + '" aria-label="Current plan commitment">' +
1161
1196
  '<div class="billingCommitmentIdentity"><span>' + esc(statusLabel) + '</span><strong>' + esc(planLabel) + '</strong></div>' +
1162
1197
  '<div class="billingCommitmentFacts">' +
1163
- '<span><b>' + esc(number(limit)) + '</b> coding-session imports</span>' +
1198
+ '<span><b>' + esc(limit === null ? "Plan import allowance" : quotaNumber(limit)) + '</b>' + (limit === null ? "" : " coding-session imports") + '</span>' +
1164
1199
  '<span><b>' + esc(moneyFact) + '</b></span>' +
1165
1200
  '</div>' +
1166
1201
  '<p>' + esc(lifecycle || "Your paid plan is active.") + '</p>' +
@@ -1171,6 +1206,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1171
1206
  }
1172
1207
  function setupPlanDefinition(plan) {
1173
1208
  var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
1209
+ var limits = quotaLimitsForPlan(plan);
1210
+ var features = limits
1211
+ ? [
1212
+ quotaNumber(limits.historicalConversationLimit) + " past chats",
1213
+ quotaTokens(limits.memoryProcessingInputTokensWeeklyLimit) + " new-chat tokens / week",
1214
+ quotaNumber(limits.memorySearchWeeklyLimit) + " memory recalls / week"
1215
+ ]
1216
+ : ["Past-chat imports", "Weekly new-chat processing", "Weekly memory recalls"];
1174
1217
  if (plan === "power") return {
1175
1218
  id: "power",
1176
1219
  name: "Power",
@@ -1180,11 +1223,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1180
1223
  cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
1181
1224
  sticker: "/hud-assets/echo-pricing-power-sticker.png",
1182
1225
  stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
1183
- features: [
1184
- "2,000 past chats",
1185
- "agent-heavy memory",
1186
- "2,000 memory recalls / week"
1187
- ]
1226
+ features: features
1188
1227
  };
1189
1228
  if (plan === "pro") return {
1190
1229
  id: "pro",
@@ -1196,11 +1235,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1196
1235
  sticker: "/hud-assets/echo-pricing-pro-sticker.png",
1197
1236
  stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
1198
1237
  popular: true,
1199
- features: [
1200
- "500 past chats",
1201
- "daily memory updates",
1202
- "500 memory recalls / week"
1203
- ]
1238
+ features: features
1204
1239
  };
1205
1240
  return {
1206
1241
  id: "free",
@@ -1211,7 +1246,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1211
1246
  cadence: "free forever · no card needed",
1212
1247
  sticker: "/hud-assets/echo-pricing-free-sticker.png",
1213
1248
  stickerAlt: "Original Echo hugs one simple memory card.",
1214
- features: ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"]
1249
+ features: features
1215
1250
  };
1216
1251
  }
1217
1252
  function renderPlanHabitat() {
@@ -1333,7 +1368,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1333
1368
  if (setupPlanChoice === "free") {
1334
1369
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1335
1370
  if (planGateDecision) planGateDecision.classList.remove("is-checkout-pending");
1336
- slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 coding sessions and 100 memory recalls each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1371
+ var freeLimits = quotaLimitsForPlan("free");
1372
+ var freePlanCopy = freeLimits
1373
+ ? quotaNumber(freeLimits.historicalConversationLimit) + " coding sessions and " + quotaNumber(freeLimits.memorySearchWeeklyLimit) + " memory recalls each week."
1374
+ : "Your managed import and weekly memory allowances are ready.";
1375
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>' + esc(freePlanCopy) + '</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1337
1376
  renderBillingCommitment("");
1338
1377
  var changeBtn = document.getElementById("changeSetupPlan");
1339
1378
  if (changeBtn) changeBtn.onclick = function () {
@@ -1434,6 +1473,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1434
1473
  var nextBillingStatus = await getJson("/billing-status");
1435
1474
  if (billingEpoch !== accountStateEpoch || !connected) return;
1436
1475
  billingStatus = nextBillingStatus;
1476
+ await refreshPlanQuotaCatalog();
1437
1477
  billingLastCheckedAt = Date.now();
1438
1478
  billingCheckMessage = "";
1439
1479
  if (readyStage === "sessions" && setupPlanConfirmed()) {
package/dist/setup.js CHANGED
@@ -309,12 +309,51 @@ export function codexTomlBlock(entry) {
309
309
  const args = (Array.isArray(entry.args) ? entry.args : []).map((a) => JSON.stringify(String(a))).join(", ");
310
310
  return `[mcp_servers.echomem]\ncommand = ${command}\nargs = [${args}]\n`;
311
311
  }
312
- /**
313
- * Write/merge the EchoMem entry straight into Codex's config.toml — no `codex` CLI needed. Idempotent.
314
- * If an `[mcp_servers.echomem]` block already exists it is REPLACED (so re-running `setup` upgrades a
315
- * stale `npx -y` entry to the direct path); identical entries are left untouched.
316
- */
317
- export function writeCodexConfig(configPath, entry) {
312
+ function objectRecord(value) {
313
+ return value && typeof value === "object" && !Array.isArray(value)
314
+ ? value
315
+ : undefined;
316
+ }
317
+ function validDesktopManagedEntry(value) {
318
+ const entry = objectRecord(value);
319
+ const environment = objectRecord(entry?.env);
320
+ if (!entry || environment?.ECHO_DESKTOP_MANAGED !== "1")
321
+ return false;
322
+ const command = entry.command;
323
+ const args = Array.isArray(entry.args) ? entry.args : [];
324
+ return typeof command === "string"
325
+ && fs.existsSync(command)
326
+ && typeof args[0] === "string"
327
+ && fs.existsSync(args[0]);
328
+ }
329
+ function codexEntryFromBlock(lines, start, end) {
330
+ let command;
331
+ let args = [];
332
+ let desktopManaged = false;
333
+ for (const line of lines.slice(start + 1, end)) {
334
+ const commandMatch = line.match(/^\s*command\s*=\s*(.+?)\s*$/);
335
+ const argsMatch = line.match(/^\s*args\s*=\s*(.+?)\s*$/);
336
+ try {
337
+ if (commandMatch)
338
+ command = JSON.parse(commandMatch[1]);
339
+ if (argsMatch)
340
+ args = JSON.parse(argsMatch[1]);
341
+ }
342
+ catch {
343
+ return undefined;
344
+ }
345
+ if (/ECHO_DESKTOP_MANAGED\s*=\s*["']1["']/.test(line))
346
+ desktopManaged = true;
347
+ }
348
+ if (typeof command !== "string")
349
+ return undefined;
350
+ return {
351
+ command,
352
+ args: Array.isArray(args) ? args : [],
353
+ ...(desktopManaged ? { env: { ECHO_DESKTOP_MANAGED: "1" } } : {}),
354
+ };
355
+ }
356
+ export function writeCodexConfig(configPath, entry, options = {}) {
318
357
  let content = "";
319
358
  try {
320
359
  content = fs.readFileSync(configPath, "utf8");
@@ -331,6 +370,9 @@ export function writeCodexConfig(configPath, entry) {
331
370
  let end = start + 1;
332
371
  while (end < lines.length && !/^\s*\[/.test(lines[end]))
333
372
  end++;
373
+ if (!options.forceHeadless && validDesktopManagedEntry(codexEntryFromBlock(lines, start, end))) {
374
+ return "desktop-managed";
375
+ }
334
376
  if (lines.slice(start, end).join("\n").trimEnd() === block)
335
377
  return "exists"; // already correct
336
378
  const next = [...lines.slice(0, start), ...block.split("\n"), ...lines.slice(end)];
@@ -403,7 +445,7 @@ export function writeAgentsMemoryGuidance(filePath) {
403
445
  return "wrote";
404
446
  }
405
447
  /** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
406
- export function writeJsonClientConfig(configPath, entry) {
448
+ export function writeJsonClientConfig(configPath, entry, options = {}) {
407
449
  let config = {};
408
450
  try {
409
451
  config = JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -412,9 +454,13 @@ export function writeJsonClientConfig(configPath, entry) {
412
454
  /* fresh config */
413
455
  }
414
456
  config.mcpServers = config.mcpServers || {};
457
+ if (!options.forceHeadless && validDesktopManagedEntry(config.mcpServers.echomem)) {
458
+ return "desktop-managed";
459
+ }
415
460
  config.mcpServers.echomem = entry;
416
461
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
417
462
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
463
+ return "wrote";
418
464
  }
419
465
  function readClaudeCodeConfigFile(configPath) {
420
466
  try {
@@ -428,12 +474,7 @@ function readClaudeCodeConfigFile(configPath) {
428
474
  }
429
475
  }
430
476
  function echoMemEntryFromServers(value) {
431
- if (!value || typeof value !== "object" || Array.isArray(value))
432
- return undefined;
433
- const entry = value.echomem;
434
- return entry && typeof entry === "object" && !Array.isArray(entry)
435
- ? entry
436
- : undefined;
477
+ return objectRecord(objectRecord(value)?.echomem);
437
478
  }
438
479
  function claudeEntriesMatch(actual, expected) {
439
480
  if (!actual || actual.command !== expected.command)
@@ -475,6 +516,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
475
516
  skippedLocalProjects: [],
476
517
  failedLocalProjects: [],
477
518
  restoredPreviousUserEntry: false,
519
+ preservedDesktopManaged: false,
478
520
  });
479
521
  const runClaude = (args, cwd) => {
480
522
  try {
@@ -496,20 +538,23 @@ export function writeClaudeCodeConfig(entry, options = {}) {
496
538
  const removeUser = () => runClaude(["mcp", "remove", "echomem", "-s", "user"]);
497
539
  const before = readClaudeCodeConfigFile(configPath);
498
540
  const previousUserEntry = echoMemEntryFromServers(before.mcpServers);
541
+ const preservedDesktopManaged = !options.forceHeadless
542
+ && validDesktopManagedEntry(previousUserEntry);
543
+ const desiredUserEntry = preservedDesktopManaged ? previousUserEntry : entry;
499
544
  let restoredPreviousUserEntry = false;
500
545
  // Avoid interrupting active/new sessions when the correct global entry is already installed.
501
- if (!claudeEntriesMatch(previousUserEntry, entry)) {
546
+ if (!claudeEntriesMatch(previousUserEntry, desiredUserEntry)) {
502
547
  if (previousUserEntry && !removeUser())
503
548
  return emptyResult();
504
- if (!addUser(entry)) {
549
+ if (!addUser(desiredUserEntry)) {
505
550
  if (previousUserEntry)
506
551
  restoredPreviousUserEntry = addUser(previousUserEntry);
507
552
  return { ...emptyResult(), restoredPreviousUserEntry };
508
553
  }
509
554
  }
510
555
  const installedUserEntry = echoMemEntryFromServers(readClaudeCodeConfigFile(configPath).mcpServers);
511
- if (!claudeEntriesMatch(installedUserEntry, entry)) {
512
- return { ...emptyResult(), restoredPreviousUserEntry };
556
+ if (!claudeEntriesMatch(installedUserEntry, desiredUserEntry)) {
557
+ return { ...emptyResult(), restoredPreviousUserEntry, preservedDesktopManaged };
513
558
  }
514
559
  const removedLocalProjects = [];
515
560
  const skippedLocalProjects = [];
@@ -544,6 +589,7 @@ export function writeClaudeCodeConfig(entry, options = {}) {
544
589
  skippedLocalProjects,
545
590
  failedLocalProjects: unresolved,
546
591
  restoredPreviousUserEntry,
592
+ preservedDesktopManaged,
547
593
  };
548
594
  }
549
595
  function readJsonClientEntry(configPath) {
@@ -2747,6 +2793,8 @@ async function cmdSetup(flags) {
2747
2793
  const entry = buildServerEntry({ devEntryPath: typeof flags.dev === "string" ? flags.dev : undefined });
2748
2794
  const requested = typeof flags.client === "string" ? flags.client : undefined;
2749
2795
  const targets = selectSetupTargets(requested, Boolean(flags.all));
2796
+ // --dev is already an explicit request to replace the managed runtime with a checkout.
2797
+ const forceHeadless = flags["force-headless"] === true || typeof flags.dev === "string";
2750
2798
  const configurationFailures = [];
2751
2799
  if (targets.length === 0) {
2752
2800
  console.log("No client auto-detected. Add this MCP server entry manually:\n");
@@ -2756,20 +2804,34 @@ async function cmdSetup(flags) {
2756
2804
  else {
2757
2805
  for (const c of targets) {
2758
2806
  if (c.kind === "json") {
2759
- writeJsonClientConfig(c.configPath, entry);
2760
- console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
2807
+ const result = writeJsonClientConfig(c.configPath, entry, { forceHeadless });
2808
+ if (result === "desktop-managed") {
2809
+ console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
2810
+ }
2811
+ else {
2812
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
2813
+ }
2761
2814
  }
2762
2815
  else if (c.kind === "command") {
2763
- const result = writeCodexConfig(c.configPath, entry);
2816
+ const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
2764
2817
  if (result === "wrote")
2765
2818
  console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
2819
+ else if (result === "desktop-managed")
2820
+ console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
2766
2821
  else
2767
2822
  console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
2768
2823
  }
2769
2824
  else {
2770
- const result = c.id === "claude-code" ? writeClaudeCodeConfig(entry) : "unavailable";
2825
+ const result = c.id === "claude-code"
2826
+ ? writeClaudeCodeConfig(entry, { forceHeadless })
2827
+ : "unavailable";
2771
2828
  if (result !== "unavailable" && result.state === "wrote") {
2772
- console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
2829
+ if (result.preservedDesktopManaged) {
2830
+ console.log(`✅ Kept the valid Echo Desktop-managed EchoMem user entry for ${c.label}.`);
2831
+ }
2832
+ else {
2833
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
2834
+ }
2773
2835
  if (result.removedLocalProjects.length > 0) {
2774
2836
  console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
2775
2837
  }
@@ -3884,6 +3946,7 @@ Usage:
3884
3946
  echomem-mcp Run the MCP server (stdio; default — used by your editor)
3885
3947
  echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
3886
3948
  echomem-mcp setup --skip-login Write MCP config without opening login/browser
3949
+ echomem-mcp setup --force-headless Explicitly replace valid Echo Desktop-managed entries
3887
3950
  echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
3888
3951
  echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
3889
3952
  echomem-mcp update --client X Repoint one MCP client; no login/browser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.42",
3
+ "version": "1.4.43",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",