@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 +71 -10
- package/dist/setup-page/client-extraction.js +54 -14
- package/dist/setup.js +85 -22
- package/package.json +1 -1
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
746
|
-
this.encConfigPromise
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
829
|
+
if (this.whoamiCache === request)
|
|
830
|
+
this.whoamiCache = null;
|
|
788
831
|
throw error;
|
|
789
832
|
});
|
|
833
|
+
this.whoamiCache = request;
|
|
790
834
|
}
|
|
791
|
-
|
|
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 :
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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,
|
|
546
|
+
if (!claudeEntriesMatch(previousUserEntry, desiredUserEntry)) {
|
|
502
547
|
if (previousUserEntry && !removeUser())
|
|
503
548
|
return emptyResult();
|
|
504
|
-
if (!addUser(
|
|
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,
|
|
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
|
-
|
|
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"
|
|
2825
|
+
const result = c.id === "claude-code"
|
|
2826
|
+
? writeClaudeCodeConfig(entry, { forceHeadless })
|
|
2827
|
+
: "unavailable";
|
|
2771
2828
|
if (result !== "unavailable" && result.state === "wrote") {
|
|
2772
|
-
|
|
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