@echomem/mcp 1.4.41 → 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 +125 -13
- package/dist/setup-page/client-extraction.js +54 -14
- package/dist/setup.js +210 -51
- package/dist/v1-contract.js +53 -3
- package/package.json +2 -2
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 {
|
|
@@ -1096,6 +1144,8 @@ class EchoMemApiClient {
|
|
|
1096
1144
|
kPerUser: parsed.kPerUser,
|
|
1097
1145
|
similarityThreshold: parsed.similarityThreshold,
|
|
1098
1146
|
timeFrameDays: parsed.timeFrameDays,
|
|
1147
|
+
workspaceId: parsed.workspaceId ?? parsed.groupId,
|
|
1148
|
+
scope: parsed.scope,
|
|
1099
1149
|
requestId: this.sessionId,
|
|
1100
1150
|
source: "mcp_friend_public_memory_search",
|
|
1101
1151
|
});
|
|
@@ -1108,7 +1158,14 @@ class EchoMemApiClient {
|
|
|
1108
1158
|
async getPublicMemory(args) {
|
|
1109
1159
|
const parsed = publicMemorySchema.parse(args ?? {});
|
|
1110
1160
|
try {
|
|
1111
|
-
const
|
|
1161
|
+
const workspaceId = parsed.workspaceId ?? parsed.groupId;
|
|
1162
|
+
const query = new URLSearchParams({
|
|
1163
|
+
requestId: this.sessionId,
|
|
1164
|
+
source: "mcp_friend_public_memory_fetch",
|
|
1165
|
+
});
|
|
1166
|
+
if (workspaceId)
|
|
1167
|
+
query.set("workspaceId", workspaceId);
|
|
1168
|
+
const response = await this.axios.get(`/api/extension/social/public-memories/${encodeURIComponent(parsed.memoryId)}?${query.toString()}`);
|
|
1112
1169
|
return response.data;
|
|
1113
1170
|
}
|
|
1114
1171
|
catch (error) {
|
|
@@ -1131,9 +1188,13 @@ class EchoMemApiClient {
|
|
|
1131
1188
|
}
|
|
1132
1189
|
}
|
|
1133
1190
|
async getGroupContext(args) {
|
|
1134
|
-
groupContextSchema.parse(args ?? {});
|
|
1191
|
+
const parsed = groupContextSchema.parse(args ?? {});
|
|
1135
1192
|
try {
|
|
1136
|
-
const
|
|
1193
|
+
const workspaceId = parsed.workspaceId ?? parsed.groupId;
|
|
1194
|
+
const path = workspaceId
|
|
1195
|
+
? `/api/extension/social/groups/current?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
1196
|
+
: "/api/extension/social/groups/current";
|
|
1197
|
+
const response = await this.axios.get(path);
|
|
1137
1198
|
return response.data;
|
|
1138
1199
|
}
|
|
1139
1200
|
catch (error) {
|
|
@@ -1197,6 +1258,7 @@ class EchoMemApiClient {
|
|
|
1197
1258
|
const enc = await this.encState();
|
|
1198
1259
|
try {
|
|
1199
1260
|
const response = await this.axios.post(`/api/extension/social/groups/current/memories/${encodeURIComponent(parsed.memoryId)}/publish`, {
|
|
1261
|
+
workspaceId: parsed.workspaceId ?? parsed.groupId,
|
|
1200
1262
|
acknowledgedFlaggedMemoryIds: parsed.acknowledgedFlaggedMemoryIds,
|
|
1201
1263
|
}, {
|
|
1202
1264
|
headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
|
|
@@ -1212,6 +1274,7 @@ class EchoMemApiClient {
|
|
|
1212
1274
|
const enc = await this.encState();
|
|
1213
1275
|
try {
|
|
1214
1276
|
const response = await this.axios.post("/api/extension/social/groups/current/memories/publish-batch", {
|
|
1277
|
+
workspaceId: parsed.workspaceId ?? parsed.groupId,
|
|
1215
1278
|
memoryIds: parsed.memoryIds,
|
|
1216
1279
|
contextId: parsed.contextId,
|
|
1217
1280
|
selectionReason: parsed.selectionReason,
|
|
@@ -1240,6 +1303,7 @@ function capWait(pending) {
|
|
|
1240
1303
|
class EchoMemMCPServer {
|
|
1241
1304
|
server;
|
|
1242
1305
|
client;
|
|
1306
|
+
accountGeneration = 0;
|
|
1243
1307
|
mapCache = null;
|
|
1244
1308
|
groupMapCache = null;
|
|
1245
1309
|
events;
|
|
@@ -1312,8 +1376,19 @@ class EchoMemMCPServer {
|
|
|
1312
1376
|
platform_source: hostPlatform,
|
|
1313
1377
|
};
|
|
1314
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
|
+
}
|
|
1315
1389
|
setupToolHandlers() {
|
|
1316
1390
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1391
|
+
this.refreshAccountContext();
|
|
1317
1392
|
const clientVersion = this.server.getClientVersion();
|
|
1318
1393
|
this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
|
|
1319
1394
|
this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
|
|
@@ -1345,6 +1420,7 @@ class EchoMemMCPServer {
|
|
|
1345
1420
|
return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
|
|
1346
1421
|
});
|
|
1347
1422
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1423
|
+
this.refreshAccountContext();
|
|
1348
1424
|
const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
|
|
1349
1425
|
const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
|
|
1350
1426
|
const canonicalName = recallRoute.canonicalName;
|
|
@@ -1995,6 +2071,28 @@ Details: ${m.details || "N/A"}`)
|
|
|
1995
2071
|
async handleOthers(args) {
|
|
1996
2072
|
const parsed = othersSchema.parse(args ?? {});
|
|
1997
2073
|
const payload = await this.client.searchOthersMemories(args);
|
|
2074
|
+
if (payload?.requiresWorkspaceSelection === true) {
|
|
2075
|
+
const availableWorkspaces = Array.isArray(payload?.availableWorkspaces)
|
|
2076
|
+
? payload.availableWorkspaces.filter(isRecord)
|
|
2077
|
+
: [];
|
|
2078
|
+
const choices = availableWorkspaces
|
|
2079
|
+
.map((workspace) => `${readString(workspace, "name") ?? "Unnamed workspace"} (${readString(workspace, "id") ?? "unknown id"})`)
|
|
2080
|
+
.join(", ");
|
|
2081
|
+
return {
|
|
2082
|
+
content: [{
|
|
2083
|
+
type: "text",
|
|
2084
|
+
text: `You belong to ${availableWorkspaces.length} workspaces: ${choices}. Ask the user which workspace's teammates to search, then call search_others_memories again with workspaceId set to one of those ids. (To search a specific friend instead, pass that person as target.)`,
|
|
2085
|
+
}],
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
if (payload?.groupScopeUnavailable === true) {
|
|
2089
|
+
return {
|
|
2090
|
+
content: [{
|
|
2091
|
+
type: "text",
|
|
2092
|
+
text: "You are not in a company workspace, so there are no teammates to search. To search friends instead, call search_others_memories again with scope \"friends\", or pass a specific person as target.",
|
|
2093
|
+
}],
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
1998
2096
|
const memories = payload?.memories ?? [];
|
|
1999
2097
|
const authenticatedViewer = isRecord(payload?.authenticatedViewer)
|
|
2000
2098
|
? payload.authenticatedViewer
|
|
@@ -2216,6 +2314,20 @@ Details: ${m.details || "N/A"}`;
|
|
|
2216
2314
|
async handleGroupContext(args) {
|
|
2217
2315
|
groupContextSchema.parse(args ?? {});
|
|
2218
2316
|
const payload = await this.client.getGroupContext(args);
|
|
2317
|
+
if (payload?.requiresWorkspaceSelection === true) {
|
|
2318
|
+
const availableWorkspaces = Array.isArray(payload?.availableWorkspaces)
|
|
2319
|
+
? payload.availableWorkspaces.filter(isRecord)
|
|
2320
|
+
: [];
|
|
2321
|
+
const choices = availableWorkspaces
|
|
2322
|
+
.map((workspace) => `${readString(workspace, "name") ?? "Unnamed workspace"} (${readString(workspace, "id") ?? "unknown id"})`)
|
|
2323
|
+
.join(", ");
|
|
2324
|
+
return {
|
|
2325
|
+
content: [{
|
|
2326
|
+
type: "text",
|
|
2327
|
+
text: `You belong to ${availableWorkspaces.length} workspaces: ${choices}. Ask the user which workspace to show context for, then call get_group_context again with workspaceId set to one of those ids.`,
|
|
2328
|
+
}],
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2219
2331
|
const group = isRecord(payload?.group) ? payload.group : null;
|
|
2220
2332
|
const participants = Array.isArray(payload?.participants)
|
|
2221
2333
|
? payload.participants.filter(isRecord)
|
|
@@ -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,17 +454,74 @@ 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";
|
|
464
|
+
}
|
|
465
|
+
function readClaudeCodeConfigFile(configPath) {
|
|
466
|
+
try {
|
|
467
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
468
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
469
|
+
? parsed
|
|
470
|
+
: {};
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function echoMemEntryFromServers(value) {
|
|
477
|
+
return objectRecord(objectRecord(value)?.echomem);
|
|
478
|
+
}
|
|
479
|
+
function claudeEntriesMatch(actual, expected) {
|
|
480
|
+
if (!actual || actual.command !== expected.command)
|
|
481
|
+
return false;
|
|
482
|
+
const actualArgs = Array.isArray(actual.args) ? actual.args : [];
|
|
483
|
+
const expectedArgs = Array.isArray(expected.args) ? expected.args : [];
|
|
484
|
+
if (actualArgs.length !== expectedArgs.length || actualArgs.some((value, index) => value !== expectedArgs[index])) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
const expectedEnv = expected.env;
|
|
488
|
+
if (!expectedEnv || typeof expectedEnv !== "object" || Array.isArray(expectedEnv))
|
|
489
|
+
return true;
|
|
490
|
+
const actualEnv = actual.env;
|
|
491
|
+
if (!actualEnv || typeof actualEnv !== "object" || Array.isArray(actualEnv))
|
|
492
|
+
return false;
|
|
493
|
+
return Object.entries(expectedEnv).every(([key, value]) => actualEnv[key] === value);
|
|
494
|
+
}
|
|
495
|
+
function claudeCodeLocalEchoMemProjects(configPath) {
|
|
496
|
+
const projects = readClaudeCodeConfigFile(configPath).projects;
|
|
497
|
+
if (!projects || typeof projects !== "object" || Array.isArray(projects))
|
|
498
|
+
return [];
|
|
499
|
+
return Object.entries(projects)
|
|
500
|
+
.filter(([, value]) => {
|
|
501
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
502
|
+
return false;
|
|
503
|
+
return Boolean(echoMemEntryFromServers(value.mcpServers));
|
|
504
|
+
})
|
|
505
|
+
.map(([projectPath]) => projectPath)
|
|
506
|
+
.sort();
|
|
418
507
|
}
|
|
419
|
-
export function writeClaudeCodeConfig(entry) {
|
|
420
|
-
// EchoMem
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
const
|
|
508
|
+
export function writeClaudeCodeConfig(entry, options = {}) {
|
|
509
|
+
// EchoMem belongs at user scope so every Claude Code project resolves the same durable runtime.
|
|
510
|
+
// Older CLI versions wrote local/project entries, which take precedence over user scope and can
|
|
511
|
+
// keep launching a deleted npm cache or stale runtime. Migrate those only after user scope is safe.
|
|
512
|
+
const configPath = options.configPath ?? home(".claude.json");
|
|
513
|
+
const emptyResult = () => ({
|
|
514
|
+
state: "unavailable",
|
|
515
|
+
removedLocalProjects: [],
|
|
516
|
+
skippedLocalProjects: [],
|
|
517
|
+
failedLocalProjects: [],
|
|
518
|
+
restoredPreviousUserEntry: false,
|
|
519
|
+
preservedDesktopManaged: false,
|
|
520
|
+
});
|
|
521
|
+
const runClaude = (args, cwd) => {
|
|
424
522
|
try {
|
|
425
|
-
execFileSync("claude",
|
|
523
|
+
execFileSync("claude", args, {
|
|
524
|
+
cwd,
|
|
426
525
|
encoding: "utf8",
|
|
427
526
|
stdio: ["ignore", "pipe", "pipe"],
|
|
428
527
|
timeout: 10000,
|
|
@@ -433,39 +532,65 @@ export function writeClaudeCodeConfig(entry) {
|
|
|
433
532
|
return false;
|
|
434
533
|
}
|
|
435
534
|
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
// constructed, remove only EchoMem and immediately re-add it; sibling MCP servers remain.
|
|
456
|
-
try {
|
|
457
|
-
if (!removeFromScope("user"))
|
|
458
|
-
return "unavailable";
|
|
459
|
-
execFileSync("claude", addArguments, {
|
|
460
|
-
encoding: "utf8",
|
|
461
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
462
|
-
timeout: 10000,
|
|
463
|
-
});
|
|
464
|
-
return "wrote";
|
|
535
|
+
const addUser = (value) => runClaude([
|
|
536
|
+
"mcp", "add-json", "-s", "user", "echomem", JSON.stringify(value),
|
|
537
|
+
]);
|
|
538
|
+
const removeUser = () => runClaude(["mcp", "remove", "echomem", "-s", "user"]);
|
|
539
|
+
const before = readClaudeCodeConfigFile(configPath);
|
|
540
|
+
const previousUserEntry = echoMemEntryFromServers(before.mcpServers);
|
|
541
|
+
const preservedDesktopManaged = !options.forceHeadless
|
|
542
|
+
&& validDesktopManagedEntry(previousUserEntry);
|
|
543
|
+
const desiredUserEntry = preservedDesktopManaged ? previousUserEntry : entry;
|
|
544
|
+
let restoredPreviousUserEntry = false;
|
|
545
|
+
// Avoid interrupting active/new sessions when the correct global entry is already installed.
|
|
546
|
+
if (!claudeEntriesMatch(previousUserEntry, desiredUserEntry)) {
|
|
547
|
+
if (previousUserEntry && !removeUser())
|
|
548
|
+
return emptyResult();
|
|
549
|
+
if (!addUser(desiredUserEntry)) {
|
|
550
|
+
if (previousUserEntry)
|
|
551
|
+
restoredPreviousUserEntry = addUser(previousUserEntry);
|
|
552
|
+
return { ...emptyResult(), restoredPreviousUserEntry };
|
|
553
|
+
}
|
|
465
554
|
}
|
|
466
|
-
|
|
467
|
-
|
|
555
|
+
const installedUserEntry = echoMemEntryFromServers(readClaudeCodeConfigFile(configPath).mcpServers);
|
|
556
|
+
if (!claudeEntriesMatch(installedUserEntry, desiredUserEntry)) {
|
|
557
|
+
return { ...emptyResult(), restoredPreviousUserEntry, preservedDesktopManaged };
|
|
558
|
+
}
|
|
559
|
+
const removedLocalProjects = [];
|
|
560
|
+
const skippedLocalProjects = [];
|
|
561
|
+
const failedLocalProjects = [];
|
|
562
|
+
for (const projectPath of claudeCodeLocalEchoMemProjects(configPath)) {
|
|
563
|
+
// A deleted directory cannot currently shadow user scope. Do not recreate it or hand-edit
|
|
564
|
+
// ~/.claude.json, which also contains Claude account/session state.
|
|
565
|
+
if (!fs.existsSync(projectPath)) {
|
|
566
|
+
skippedLocalProjects.push(projectPath);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
let projectCwd = projectPath;
|
|
570
|
+
try {
|
|
571
|
+
projectCwd = fs.realpathSync(projectPath);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
/* The existence check above already established the safe fallback path. */
|
|
575
|
+
}
|
|
576
|
+
if (runClaude(["mcp", "remove", "echomem", "-s", "local"], projectCwd)) {
|
|
577
|
+
removedLocalProjects.push(projectPath);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
failedLocalProjects.push(projectPath);
|
|
581
|
+
}
|
|
468
582
|
}
|
|
583
|
+
const remainingActiveProjects = claudeCodeLocalEchoMemProjects(configPath)
|
|
584
|
+
.filter((projectPath) => fs.existsSync(projectPath));
|
|
585
|
+
const unresolved = [...new Set([...failedLocalProjects, ...remainingActiveProjects])].sort();
|
|
586
|
+
return {
|
|
587
|
+
state: unresolved.length > 0 ? "needs-repair" : "wrote",
|
|
588
|
+
removedLocalProjects,
|
|
589
|
+
skippedLocalProjects,
|
|
590
|
+
failedLocalProjects: unresolved,
|
|
591
|
+
restoredPreviousUserEntry,
|
|
592
|
+
preservedDesktopManaged,
|
|
593
|
+
};
|
|
469
594
|
}
|
|
470
595
|
function readJsonClientEntry(configPath) {
|
|
471
596
|
try {
|
|
@@ -2668,6 +2793,9 @@ async function cmdSetup(flags) {
|
|
|
2668
2793
|
const entry = buildServerEntry({ devEntryPath: typeof flags.dev === "string" ? flags.dev : undefined });
|
|
2669
2794
|
const requested = typeof flags.client === "string" ? flags.client : undefined;
|
|
2670
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";
|
|
2798
|
+
const configurationFailures = [];
|
|
2671
2799
|
if (targets.length === 0) {
|
|
2672
2800
|
console.log("No client auto-detected. Add this MCP server entry manually:\n");
|
|
2673
2801
|
console.log(JSON.stringify({ echomem: entry }, null, 2));
|
|
@@ -2676,27 +2804,57 @@ async function cmdSetup(flags) {
|
|
|
2676
2804
|
else {
|
|
2677
2805
|
for (const c of targets) {
|
|
2678
2806
|
if (c.kind === "json") {
|
|
2679
|
-
writeJsonClientConfig(c.configPath, entry);
|
|
2680
|
-
|
|
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
|
+
}
|
|
2681
2814
|
}
|
|
2682
2815
|
else if (c.kind === "command") {
|
|
2683
|
-
const result = writeCodexConfig(c.configPath, entry);
|
|
2816
|
+
const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
|
|
2684
2817
|
if (result === "wrote")
|
|
2685
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}`);
|
|
2686
2821
|
else
|
|
2687
2822
|
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
2688
2823
|
}
|
|
2689
2824
|
else {
|
|
2690
|
-
const result = c.id === "claude-code"
|
|
2691
|
-
|
|
2692
|
-
|
|
2825
|
+
const result = c.id === "claude-code"
|
|
2826
|
+
? writeClaudeCodeConfig(entry, { forceHeadless })
|
|
2827
|
+
: "unavailable";
|
|
2828
|
+
if (result !== "unavailable" && result.state === "wrote") {
|
|
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
|
+
}
|
|
2835
|
+
if (result.removedLocalProjects.length > 0) {
|
|
2836
|
+
console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
|
|
2837
|
+
}
|
|
2838
|
+
if (result.skippedLocalProjects.length > 0) {
|
|
2839
|
+
console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
|
|
2840
|
+
}
|
|
2693
2841
|
}
|
|
2694
2842
|
else {
|
|
2695
|
-
|
|
2843
|
+
const failedProjects = result === "unavailable" ? [] : result.failedLocalProjects;
|
|
2844
|
+
configurationFailures.push(failedProjects.length > 0
|
|
2845
|
+
? `${c.label} still has project-local EchoMem overrides in: ${failedProjects.join(", ")}`
|
|
2846
|
+
: `${c.label} user-scoped EchoMem entry could not be verified`);
|
|
2696
2847
|
}
|
|
2697
2848
|
}
|
|
2698
2849
|
}
|
|
2699
2850
|
}
|
|
2851
|
+
if (configurationFailures.length > 0) {
|
|
2852
|
+
throw new Error([
|
|
2853
|
+
"EchoMem MCP configuration is incomplete; onboarding was stopped before login/import.",
|
|
2854
|
+
...configurationFailures.map((failure) => `- ${failure}`),
|
|
2855
|
+
`Retry with: ${MCP_UPDATE_COMMAND} --client claude-code`,
|
|
2856
|
+
].join("\n"));
|
|
2857
|
+
}
|
|
2700
2858
|
if (!flags["no-agents-md"]) {
|
|
2701
2859
|
writeMemoryGuidanceForTargets(targets);
|
|
2702
2860
|
}
|
|
@@ -3788,6 +3946,7 @@ Usage:
|
|
|
3788
3946
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
3789
3947
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
3790
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
|
|
3791
3950
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
3792
3951
|
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
3793
3952
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
package/dist/v1-contract.js
CHANGED
|
@@ -165,6 +165,42 @@ const triggerMetadataSchema = {
|
|
|
165
165
|
triggerMessage: z.string().optional(),
|
|
166
166
|
triggerMessageRole: z.string().optional(),
|
|
167
167
|
};
|
|
168
|
+
// Canonical workspace selector for tools that act inside a company workspace.
|
|
169
|
+
// `workspaceId` is the current name; `groupId` is the legacy alias kept working
|
|
170
|
+
// so existing prompts keep functioning. Handlers normalize with
|
|
171
|
+
// normalizeWorkspaceId (workspaceId ?? groupId). Required only when the user
|
|
172
|
+
// belongs to more than one workspace; a single-workspace user may omit it.
|
|
173
|
+
const WORKSPACE_SELECTOR_DESCRIPTION = "Workspace to act in. Required when the user belongs to more than one workspace; omit it when they have a single workspace. If omitted with multiple workspaces, the tool returns the available workspaces so you can ask the user which to use.";
|
|
174
|
+
const workspaceSelectorSchema = {
|
|
175
|
+
workspaceId: z.string().uuid().optional().describe(WORKSPACE_SELECTOR_DESCRIPTION),
|
|
176
|
+
groupId: z.string().uuid().optional().describe("Legacy alias for workspaceId."),
|
|
177
|
+
};
|
|
178
|
+
// The advertised JSON-Schema counterpart of workspaceSelectorSchema. Tool
|
|
179
|
+
// inputSchemas are hand-written, so the selector must be injected into every
|
|
180
|
+
// workspace-scoped tool's properties or clients never learn they can pass it.
|
|
181
|
+
const workspaceSelectorProperties = {
|
|
182
|
+
workspaceId: { type: "string", description: WORKSPACE_SELECTOR_DESCRIPTION },
|
|
183
|
+
groupId: { type: "string", description: "Legacy alias for workspaceId." },
|
|
184
|
+
};
|
|
185
|
+
const WORKSPACE_SCOPED_TOOL_NAMES = new Set([
|
|
186
|
+
canonicalToolNames.others,
|
|
187
|
+
canonicalToolNames.publicMemory,
|
|
188
|
+
canonicalToolNames.groupContext,
|
|
189
|
+
canonicalToolNames.createGroupInvite,
|
|
190
|
+
canonicalToolNames.prepareGroupPublication,
|
|
191
|
+
canonicalToolNames.updateGroupProfile,
|
|
192
|
+
canonicalToolNames.publishToGroup,
|
|
193
|
+
canonicalToolNames.publishBatchToGroup,
|
|
194
|
+
]);
|
|
195
|
+
function injectWorkspaceSelector(specs) {
|
|
196
|
+
for (const spec of specs) {
|
|
197
|
+
if (!WORKSPACE_SCOPED_TOOL_NAMES.has(spec.name))
|
|
198
|
+
continue;
|
|
199
|
+
const existing = spec.inputSchema.properties ?? {};
|
|
200
|
+
spec.inputSchema.properties = { ...workspaceSelectorProperties, ...existing };
|
|
201
|
+
}
|
|
202
|
+
return specs;
|
|
203
|
+
}
|
|
168
204
|
export const searchMemoriesSchema = z.object({
|
|
169
205
|
...triggerMetadataSchema,
|
|
170
206
|
query: z.string().trim().min(1).optional(),
|
|
@@ -235,6 +271,8 @@ export const sendFriendRequestSchema = z.object({
|
|
|
235
271
|
});
|
|
236
272
|
export const othersSchema = z.object({
|
|
237
273
|
...triggerMetadataSchema,
|
|
274
|
+
...workspaceSelectorSchema,
|
|
275
|
+
scope: z.enum(["group", "friends"]).optional(),
|
|
238
276
|
query: z.string().trim().optional().default(""),
|
|
239
277
|
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
240
278
|
target: z.string().optional(),
|
|
@@ -249,6 +287,7 @@ export const othersSchema = z.object({
|
|
|
249
287
|
});
|
|
250
288
|
export const publicMemorySchema = z.object({
|
|
251
289
|
...triggerMetadataSchema,
|
|
290
|
+
...workspaceSelectorSchema,
|
|
252
291
|
memoryId: z.string().min(1),
|
|
253
292
|
});
|
|
254
293
|
export const recordMemoryCitationsSchema = z.object({
|
|
@@ -258,6 +297,7 @@ export const recordMemoryCitationsSchema = z.object({
|
|
|
258
297
|
});
|
|
259
298
|
export const groupContextSchema = z.object({
|
|
260
299
|
...triggerMetadataSchema,
|
|
300
|
+
...workspaceSelectorSchema,
|
|
261
301
|
});
|
|
262
302
|
export const getGroupSessionSharingSchema = z.object({
|
|
263
303
|
...triggerMetadataSchema,
|
|
@@ -282,6 +322,7 @@ export const createGroupSchema = z.object({
|
|
|
282
322
|
});
|
|
283
323
|
export const createGroupInviteSchema = z.object({
|
|
284
324
|
...triggerMetadataSchema,
|
|
325
|
+
...workspaceSelectorSchema,
|
|
285
326
|
expiresInDays: z.number().int().min(1).max(30).optional(),
|
|
286
327
|
maxUses: z.number().int().min(1).max(100).optional(),
|
|
287
328
|
});
|
|
@@ -294,6 +335,7 @@ export const joinGroupSchema = z.object({
|
|
|
294
335
|
});
|
|
295
336
|
export const prepareGroupPublicationSchema = z.object({
|
|
296
337
|
...triggerMetadataSchema,
|
|
338
|
+
...workspaceSelectorSchema,
|
|
297
339
|
scope: z.enum(["bootstrap", "since_last_scan", "context", "time_range"]).default("since_last_scan"),
|
|
298
340
|
contextId: z.string().min(1).optional(),
|
|
299
341
|
startAt: z.string().optional(),
|
|
@@ -310,6 +352,7 @@ export const flagPublicationAttentionSchema = z.object({
|
|
|
310
352
|
});
|
|
311
353
|
export const updateGroupProfileSchema = z.object({
|
|
312
354
|
...triggerMetadataSchema,
|
|
355
|
+
...workspaceSelectorSchema,
|
|
313
356
|
displayName: z.string().min(1).max(120).optional(),
|
|
314
357
|
title: z.string().min(1).max(160),
|
|
315
358
|
responsibilitySummary: z.string().min(1).max(1000),
|
|
@@ -325,11 +368,13 @@ export const completeGroupPublicationSchema = z.object({
|
|
|
325
368
|
});
|
|
326
369
|
export const publishToGroupSchema = z.object({
|
|
327
370
|
...triggerMetadataSchema,
|
|
371
|
+
...workspaceSelectorSchema,
|
|
328
372
|
memoryId: z.string().min(1),
|
|
329
373
|
acknowledgedFlaggedMemoryIds: z.array(z.string().min(1)).max(1).optional(),
|
|
330
374
|
});
|
|
331
375
|
export const publishBatchToGroupSchema = z.object({
|
|
332
376
|
...triggerMetadataSchema,
|
|
377
|
+
...workspaceSelectorSchema,
|
|
333
378
|
memoryIds: z.array(z.string().min(1)).min(1).max(50),
|
|
334
379
|
contextId: z.string().min(1).optional(),
|
|
335
380
|
selectionReason: z.string().max(500).optional(),
|
|
@@ -573,10 +618,15 @@ export function listToolSpecs(opts = {}) {
|
|
|
573
618
|
{
|
|
574
619
|
name: canonicalToolNames.others,
|
|
575
620
|
title: "Search teammates' and friends' memories",
|
|
576
|
-
description: `PEER-MEMORY SEARCH for public memories owned by accepted friends or company-group members—not the user's own memories. Pass query for a topic
|
|
621
|
+
description: `PEER-MEMORY SEARCH for public memories owned by accepted friends or company-group members—not the user's own memories. Pass query for a topic. Set scope to pick the audience — 'group' (only workspace teammates) or 'friends' (only friends) — or target for one person; scope defaults to teammates. Do not use this tool for the user's private memories or the EchoMem user directory. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inference. For onboarding and division-of-work questions, call get_group_context first. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
|
|
577
622
|
inputSchema: {
|
|
578
623
|
type: "object",
|
|
579
624
|
properties: {
|
|
625
|
+
scope: {
|
|
626
|
+
type: "string",
|
|
627
|
+
enum: ["group", "friends"],
|
|
628
|
+
description: "Audience to search. 'group' = only the named workspace's teammates (you'll be asked which if the user is in several and names none). 'friends' = only accepted friends. For one specific person, omit scope and pass target instead. Omitting scope defaults to the group/teammates audience.",
|
|
629
|
+
},
|
|
580
630
|
query: {
|
|
581
631
|
type: "string",
|
|
582
632
|
description: "Optional peer-memory topic. Omit only for an intentional broad browse; never send this field as conversation.",
|
|
@@ -584,7 +634,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
584
634
|
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
|
|
585
635
|
target: {
|
|
586
636
|
type: "string",
|
|
587
|
-
description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks.",
|
|
637
|
+
description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks or a single specific person.",
|
|
588
638
|
},
|
|
589
639
|
ownerUserId: {
|
|
590
640
|
type: "string",
|
|
@@ -1021,5 +1071,5 @@ export function listToolSpecs(opts = {}) {
|
|
|
1021
1071
|
},
|
|
1022
1072
|
},
|
|
1023
1073
|
];
|
|
1024
|
-
return tools.map(decorateLocalToolSpec);
|
|
1074
|
+
return injectWorkspaceSelector(tools).map(decorateLocalToolSpec);
|
|
1025
1075
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"test:ui": "npm run build && node test/setup-ui.test.mjs",
|
|
34
34
|
"test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
|
|
35
35
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
36
|
-
"test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
36
|
+
"test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
37
37
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|