@echomem/mcp 1.4.25 → 1.4.27
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/context-analysis/vendored-canonical.js +54 -11
- package/dist/forensics.js +24 -4
- package/dist/hud/cli.js +18 -1
- package/dist/hud/hooks.js +59 -0
- package/dist/index.js +122 -13
- package/dist/package-metadata.js +6 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +86 -15
- package/dist/setup-page/client-extraction.js +313 -145
- package/dist/setup-page/client-lifecycle.js +10 -11
- package/dist/setup-page/client-report-audit.js +3 -2
- package/dist/setup-page/client-report-city.js +36 -40
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +568 -150
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +145 -12
- package/dist/v1-contract.js +41 -8
- package/package.json +3 -2
- package/templates/codex-skills/echomem-save/SKILL.md +10 -0
- package/templates/echomem-recall.md +10 -2
|
@@ -378,6 +378,38 @@ function slimScored(row) {
|
|
|
378
378
|
repo: row.repo,
|
|
379
379
|
};
|
|
380
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* The vendored scorer's waste ledger is the canonical classified quantity. Useful context is its
|
|
383
|
+
* complement inside the provider's official input window. Deriving the complement here prevents a
|
|
384
|
+
* long floating-point accumulation from making useful + waste exceed the official window.
|
|
385
|
+
*/
|
|
386
|
+
export function reconcileCanonicalTokenPartition(officialInputTokens, wasteTokens) {
|
|
387
|
+
const input = Math.max(0, Math.round(Number(officialInputTokens) || 0));
|
|
388
|
+
const waste = Math.max(0, Math.min(input, Math.round(Number(wasteTokens) || 0)));
|
|
389
|
+
return {
|
|
390
|
+
officialInputTokens: input,
|
|
391
|
+
usefulTokens: input - waste,
|
|
392
|
+
wasteTokens: waste,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function reconcileBucketParts(values, target) {
|
|
396
|
+
const safe = values.map((value) => Math.max(0, Number(value) || 0));
|
|
397
|
+
const total = safe.reduce((sum, value) => sum + value, 0);
|
|
398
|
+
const exactTarget = Math.max(0, Math.round(target));
|
|
399
|
+
if (Math.abs(total - exactTarget) < 1e-9 && safe.every(Number.isInteger))
|
|
400
|
+
return safe;
|
|
401
|
+
if (total <= 0)
|
|
402
|
+
return safe.map((_, index) => index === 0 ? exactTarget : 0);
|
|
403
|
+
const scaled = safe.map((value) => (value / total) * exactTarget);
|
|
404
|
+
const result = scaled.map(Math.floor);
|
|
405
|
+
let remainder = exactTarget - result.reduce((sum, value) => sum + value, 0);
|
|
406
|
+
const byFraction = scaled
|
|
407
|
+
.map((value, index) => ({ index, fraction: value - Math.floor(value) }))
|
|
408
|
+
.sort((a, b) => b.fraction - a.fraction || a.index - b.index);
|
|
409
|
+
for (let index = 0; index < remainder; index++)
|
|
410
|
+
result[byFraction[index % byFraction.length].index] += 1;
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
381
413
|
function aggregate(scored, metadata) {
|
|
382
414
|
const totals = scored.reduce((sum, row) => {
|
|
383
415
|
const value = row.dashboard.totals;
|
|
@@ -402,6 +434,17 @@ function aggregate(scored, metadata) {
|
|
|
402
434
|
rawBuckets.opt_dead += turn.opt_dead || 0;
|
|
403
435
|
}
|
|
404
436
|
}
|
|
437
|
+
const partition = reconcileCanonicalTokenPartition(totals.input, totals.waste);
|
|
438
|
+
const [keepOh, keepProd] = reconcileBucketParts([rawBuckets.keep_oh, rawBuckets.keep_prod], partition.usefulTokens);
|
|
439
|
+
const [optDup, optRefind, optDead] = reconcileBucketParts([rawBuckets.opt_dup, rawBuckets.opt_refind, rawBuckets.opt_dead], partition.wasteTokens);
|
|
440
|
+
const reconciledRawBuckets = {
|
|
441
|
+
keep_oh: keepOh,
|
|
442
|
+
keep_prod: keepProd,
|
|
443
|
+
opt_dup: optDup,
|
|
444
|
+
opt_refind: optRefind,
|
|
445
|
+
opt_dead: optDead,
|
|
446
|
+
};
|
|
447
|
+
const [duplicate, refind, dead, unattributed] = reconcileBucketParts([totals.duplicate, totals.refind, totals.dead, totals.unattributed], partition.wasteTokens);
|
|
405
448
|
const problemRows = new Map();
|
|
406
449
|
for (const id of PROBLEM_IDS)
|
|
407
450
|
problemRows.set(id, { tokens: 0, count: 0, pressure: 0, sessions: 0, examples: [] });
|
|
@@ -517,19 +560,19 @@ function aggregate(scored, metadata) {
|
|
|
517
560
|
sessionsAnalyzed: scored.length,
|
|
518
561
|
reposAnalyzed: repoRows.size,
|
|
519
562
|
turnsAnalyzed: totals.turns,
|
|
520
|
-
officialInputTokens:
|
|
521
|
-
usefulTokens:
|
|
522
|
-
wasteTokens:
|
|
523
|
-
usefulPct: percentage(
|
|
524
|
-
wastePct: percentage(
|
|
525
|
-
attributedWasteTokens:
|
|
526
|
-
unattributedWasteTokens:
|
|
527
|
-
rawUsefulTokens:
|
|
528
|
-
rawOutcomeResidueTokens:
|
|
563
|
+
officialInputTokens: partition.officialInputTokens,
|
|
564
|
+
usefulTokens: partition.usefulTokens,
|
|
565
|
+
wasteTokens: partition.wasteTokens,
|
|
566
|
+
usefulPct: percentage(partition.usefulTokens, partition.officialInputTokens),
|
|
567
|
+
wastePct: percentage(partition.wasteTokens, partition.officialInputTokens),
|
|
568
|
+
attributedWasteTokens: partition.wasteTokens - unattributed,
|
|
569
|
+
unattributedWasteTokens: unattributed,
|
|
570
|
+
rawUsefulTokens: keepOh + keepProd,
|
|
571
|
+
rawOutcomeResidueTokens: optDup + optRefind + optDead,
|
|
529
572
|
excludedUnlabeledTokens: totals.excluded,
|
|
530
573
|
},
|
|
531
|
-
buckets: { duplicate
|
|
532
|
-
rawBuckets,
|
|
574
|
+
buckets: { duplicate, refind, dead, unattributed },
|
|
575
|
+
rawBuckets: reconciledRawBuckets,
|
|
533
576
|
problems,
|
|
534
577
|
sessions,
|
|
535
578
|
repos: [...repoRows.entries()].map(([repo, values]) => ({ repo, ...values })).sort((a, b) => b.officialInputTokens - a.officialInputTokens),
|
package/dist/forensics.js
CHANGED
|
@@ -1373,8 +1373,25 @@ export async function buildForensicReport(opts) {
|
|
|
1373
1373
|
: [];
|
|
1374
1374
|
const total = codexFiles.length + claudeFiles.length;
|
|
1375
1375
|
let done = 0;
|
|
1376
|
-
|
|
1377
|
-
|
|
1376
|
+
// Every stage reports the same file counters as scanned/total, so the visible "N of M sessions
|
|
1377
|
+
// scanned" only ever climbs. A stage's own counters (the canonical pass counts a different set,
|
|
1378
|
+
// on a different scale) drive nothing but its slice of the overall bar — feeding them straight
|
|
1379
|
+
// into scanned/total is what made the bar and the caption fall back partway through the scan.
|
|
1380
|
+
const STAGE_BANDS = {
|
|
1381
|
+
"reading-transcripts": [0, 0.7],
|
|
1382
|
+
"building-summary": [0.7, 0.75],
|
|
1383
|
+
"classifying-repeated-context": [0.75, 0.95],
|
|
1384
|
+
"finalizing-report": [0.95, 1],
|
|
1385
|
+
};
|
|
1386
|
+
const overallFor = (stage, stageDone, stageTotal) => {
|
|
1387
|
+
const band = STAGE_BANDS[stage];
|
|
1388
|
+
if (!band)
|
|
1389
|
+
return 0;
|
|
1390
|
+
const ratio = stageTotal > 0 ? Math.min(1, Math.max(0, stageDone / stageTotal)) : 0;
|
|
1391
|
+
return band[0] + (band[1] - band[0]) * ratio;
|
|
1392
|
+
};
|
|
1393
|
+
const progress = (stage, detail, stageDone = done, stageTotal = total) => {
|
|
1394
|
+
opts?.onProgress?.(done, total, stage, detail, overallFor(stage, stageDone, stageTotal));
|
|
1378
1395
|
};
|
|
1379
1396
|
progress("reading-transcripts", "finding local Codex and Claude transcript files");
|
|
1380
1397
|
const tick = (stage = "reading-transcripts") => {
|
|
@@ -1417,7 +1434,10 @@ export async function buildForensicReport(opts) {
|
|
|
1417
1434
|
timelineFiles.sort((a, b) => (a.fe.firstTs ?? Number.POSITIVE_INFINITY) - (b.fe.firstTs ?? Number.POSITIVE_INFINITY) ||
|
|
1418
1435
|
a.path.localeCompare(b.path));
|
|
1419
1436
|
replayTimelineFiles(eng, timelineFiles);
|
|
1420
|
-
|
|
1437
|
+
// Entering a stage means it has done none of its own work yet, so it opens its band rather than
|
|
1438
|
+
// inheriting the finished file counters — otherwise a stage announces itself at its band ceiling
|
|
1439
|
+
// and its real sub-progress then drags the bar back down.
|
|
1440
|
+
progress("building-summary", "aggregating rereads, model usage, cost, and local context signals", 0, 1);
|
|
1421
1441
|
const report = eng.build();
|
|
1422
1442
|
const firstTimelineSession = timelineFiles.find(({ fe }) => fe.firstTs != null);
|
|
1423
1443
|
report.firstSession = firstTimelineSession ? {
|
|
@@ -1461,7 +1481,7 @@ export async function buildForensicReport(opts) {
|
|
|
1461
1481
|
canonicalSources.push("codex");
|
|
1462
1482
|
if (sources.includes("claude"))
|
|
1463
1483
|
canonicalSources.push("claude-code");
|
|
1464
|
-
progress("classifying-repeated-context", "reconstructing context windows and attributing P01/P03/P08/P10/P13 waste");
|
|
1484
|
+
progress("classifying-repeated-context", "reconstructing context windows and attributing P01/P03/P08/P10/P13 waste", 0, 1);
|
|
1465
1485
|
report.canonicalGoldenStandard = await buildCanonicalGoldenReport({
|
|
1466
1486
|
sources: canonicalSources,
|
|
1467
1487
|
codexSessionPaths: canonicalCodexFiles,
|
package/dist/hud/cli.js
CHANGED
|
@@ -5,11 +5,12 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { adapterList } from "./adapters.js";
|
|
6
6
|
import { autostartPlistPath, autostartSupported, disableAutostart, enableAutostart, isAutostartEnabled } from "./autostart.js";
|
|
7
7
|
import { statSignature } from "./fs.js";
|
|
8
|
-
import { installHooks } from "./hooks.js";
|
|
8
|
+
import { installHooks, installSaveCheckpointHooks } from "./hooks.js";
|
|
9
9
|
import { HudMonitor } from "./monitor.js";
|
|
10
10
|
import { renderStateText } from "./render.js";
|
|
11
11
|
import { renderReportText, runReport } from "./report.js";
|
|
12
12
|
import { createHudServer } from "./server.js";
|
|
13
|
+
import { runSaveCheckpointHook } from "../save-checkpoint-hook.js";
|
|
13
14
|
const argv = process.argv.slice(2);
|
|
14
15
|
const command = argv[0] || "summary";
|
|
15
16
|
const flags = parseFlags(argv.slice(1));
|
|
@@ -24,6 +25,10 @@ try {
|
|
|
24
25
|
await cmdApp(flags);
|
|
25
26
|
else if (command === "install-hooks")
|
|
26
27
|
await cmdInstallHooks(flags);
|
|
28
|
+
else if (command === "install-save-hooks")
|
|
29
|
+
await cmdInstallSaveHooks(flags);
|
|
30
|
+
else if (command === "save-checkpoint")
|
|
31
|
+
await cmdSaveCheckpoint();
|
|
27
32
|
else if (command === "autostart")
|
|
28
33
|
cmdAutostart(argv[1] || "status", flags);
|
|
29
34
|
else if (command === "status")
|
|
@@ -114,6 +119,17 @@ async function cmdInstallHooks(flags) {
|
|
|
114
119
|
console.log(`Installed EchoMem HUD hook support:\n${paths.map((p) => `- ${p}`).join("\n")}`);
|
|
115
120
|
console.log("Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
|
|
116
121
|
}
|
|
122
|
+
async function cmdInstallSaveHooks(flags) {
|
|
123
|
+
const paths = installSaveCheckpointHooks(parseMode(flags.client));
|
|
124
|
+
console.log(`Installed EchoMem private-save checkpoint hooks:\n${paths.map((p) => `- ${p}`).join("\n")}`);
|
|
125
|
+
console.log("Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
|
|
126
|
+
}
|
|
127
|
+
async function cmdSaveCheckpoint() {
|
|
128
|
+
let input = "";
|
|
129
|
+
for await (const chunk of process.stdin)
|
|
130
|
+
input += String(chunk);
|
|
131
|
+
process.stdout.write(runSaveCheckpointHook(input));
|
|
132
|
+
}
|
|
117
133
|
async function cmdReport(flags) {
|
|
118
134
|
const result = runReport(parseMode(flags.client), { limit: readNumber(flags.limit, 40) });
|
|
119
135
|
if (flags.json)
|
|
@@ -166,6 +182,7 @@ Usage:
|
|
|
166
182
|
echomem-hud serve [--client codex|claude-code|claude-desktop|both|auto] [--port 17377]
|
|
167
183
|
echomem-hud app [--client codex|claude-code|claude-desktop|both|auto]
|
|
168
184
|
echomem-hud install-hooks [--client codex|claude-code|both]
|
|
185
|
+
echomem-hud install-save-hooks [--client codex|claude-code|both]
|
|
169
186
|
echomem-hud autostart on|off|status (show the HUD after restart — macOS)
|
|
170
187
|
echomem-hud status
|
|
171
188
|
echomem-hud report [--client codex|claude-code|claude-desktop|auto] [--limit 40] [--json]
|
package/dist/hud/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
export function installHooks(mode) {
|
|
5
6
|
const written = [];
|
|
6
7
|
if (mode === "codex" || mode === "both" || mode === "auto") {
|
|
@@ -11,6 +12,16 @@ export function installHooks(mode) {
|
|
|
11
12
|
}
|
|
12
13
|
return written;
|
|
13
14
|
}
|
|
15
|
+
export function installSaveCheckpointHooks(mode) {
|
|
16
|
+
const written = [];
|
|
17
|
+
if (mode === "codex" || mode === "both" || mode === "auto") {
|
|
18
|
+
written.push(installCodexSaveCheckpointHook());
|
|
19
|
+
}
|
|
20
|
+
if (mode === "claude-code" || mode === "both" || mode === "auto") {
|
|
21
|
+
written.push(installClaudeCodeSaveCheckpointHook());
|
|
22
|
+
}
|
|
23
|
+
return written;
|
|
24
|
+
}
|
|
14
25
|
function installCodexHooks() {
|
|
15
26
|
const dir = path.join(os.homedir(), ".codex");
|
|
16
27
|
const file = path.join(dir, "hooks.json");
|
|
@@ -24,6 +35,44 @@ function installCodexHooks() {
|
|
|
24
35
|
fs.writeFileSync(file, JSON.stringify(content, null, 2));
|
|
25
36
|
return file;
|
|
26
37
|
}
|
|
38
|
+
function saveCheckpointCommand() {
|
|
39
|
+
const lifecycleCli = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
40
|
+
return `${JSON.stringify(process.execPath)} ${JSON.stringify(lifecycleCli)} save-checkpoint`;
|
|
41
|
+
}
|
|
42
|
+
function installCodexSaveCheckpointHook() {
|
|
43
|
+
const dir = path.join(os.homedir(), ".codex");
|
|
44
|
+
const file = path.join(dir, "hooks.json");
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
const content = readHooksFile(file);
|
|
47
|
+
content.hooks = content.hooks || {};
|
|
48
|
+
content.hooks.Stop = mergeSaveCheckpointGroup(content.hooks.Stop, {
|
|
49
|
+
hooks: [{
|
|
50
|
+
type: "command",
|
|
51
|
+
command: saveCheckpointCommand(),
|
|
52
|
+
timeout: 10,
|
|
53
|
+
statusMessage: "Checking whether completed work should be remembered",
|
|
54
|
+
}],
|
|
55
|
+
});
|
|
56
|
+
fs.writeFileSync(file, JSON.stringify(content, null, 2));
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
function installClaudeCodeSaveCheckpointHook() {
|
|
60
|
+
const dir = path.join(os.homedir(), ".claude");
|
|
61
|
+
const file = path.join(dir, "settings.json");
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
const content = readHooksFile(file);
|
|
64
|
+
content.hooks = content.hooks || {};
|
|
65
|
+
content.hooks.Stop = mergeSaveCheckpointGroup(content.hooks.Stop, {
|
|
66
|
+
hooks: [{
|
|
67
|
+
type: "command",
|
|
68
|
+
command: saveCheckpointCommand(),
|
|
69
|
+
timeout: 10,
|
|
70
|
+
statusMessage: "Checking whether completed work should be remembered",
|
|
71
|
+
}],
|
|
72
|
+
});
|
|
73
|
+
fs.writeFileSync(file, JSON.stringify(content, null, 2));
|
|
74
|
+
return file;
|
|
75
|
+
}
|
|
27
76
|
function installClaudeCodeSnippet() {
|
|
28
77
|
const dir = path.join(os.homedir(), ".claude", "echo-ctx");
|
|
29
78
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -43,8 +92,18 @@ function mergeHookGroup(existing, group) {
|
|
|
43
92
|
groups.push(group);
|
|
44
93
|
return groups;
|
|
45
94
|
}
|
|
95
|
+
function mergeSaveCheckpointGroup(existing, group) {
|
|
96
|
+
const groups = Array.isArray(existing) ? existing.filter((item) => !isEchoSaveCheckpointGroup(item)) : [];
|
|
97
|
+
groups.push(group);
|
|
98
|
+
return groups;
|
|
99
|
+
}
|
|
46
100
|
function isEchoHudGroup(value) {
|
|
47
101
|
if (typeof value !== "object" || value === null)
|
|
48
102
|
return false;
|
|
49
103
|
return JSON.stringify(value).includes("summary --client codex --json");
|
|
50
104
|
}
|
|
105
|
+
function isEchoSaveCheckpointGroup(value) {
|
|
106
|
+
if (typeof value !== "object" || value === null)
|
|
107
|
+
return false;
|
|
108
|
+
return JSON.stringify(value).includes("save-checkpoint");
|
|
109
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
6
|
import { ZodError } from "zod";
|
|
7
|
-
import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, } from "./v1-contract.js";
|
|
7
|
+
import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
8
8
|
import { KeyStore } from "./keystore.js";
|
|
9
9
|
import { EventLogger, hashText } from "./events.js";
|
|
10
10
|
import { buildReportText } from "./report.js";
|
|
@@ -17,9 +17,20 @@ import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
|
|
|
17
17
|
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
18
18
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
19
19
|
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
|
|
20
|
-
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/
|
|
20
|
+
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
21
21
|
function memoryWebUrl(memoryId) {
|
|
22
|
-
return `${ECHO_MEMORY_WEB_URL}
|
|
22
|
+
return `${ECHO_MEMORY_WEB_URL}/${encodeURIComponent(memoryId)}`;
|
|
23
|
+
}
|
|
24
|
+
function personalMemoryWebUrl(memoryId) {
|
|
25
|
+
return memoryWebUrl(memoryId);
|
|
26
|
+
}
|
|
27
|
+
function memoryMarkdownLink(url, keys, description) {
|
|
28
|
+
const rawLabel = String(keys || description || "Open memory").replace(/\s+/g, " ").trim();
|
|
29
|
+
const label = (rawLabel.length > 100 ? `${rawLabel.slice(0, 97).trim()}…` : rawLabel)
|
|
30
|
+
.replace(/\\/g, "\\\\")
|
|
31
|
+
.replace(/\[/g, "\\[")
|
|
32
|
+
.replace(/\]/g, "\\]");
|
|
33
|
+
return `[${label}](${url})`;
|
|
23
34
|
}
|
|
24
35
|
/** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
|
|
25
36
|
class NoTokenError extends Error {
|
|
@@ -507,6 +518,11 @@ function inputAnalyticsForTool(canonicalName, args) {
|
|
|
507
518
|
confirmed: a.confirmed === true,
|
|
508
519
|
};
|
|
509
520
|
}
|
|
521
|
+
case canonicalToolNames.setGroupSessionSharing:
|
|
522
|
+
return {
|
|
523
|
+
share_to_group: typeof a.share === "boolean" ? a.share : undefined,
|
|
524
|
+
confirmed: a.confirmed === true,
|
|
525
|
+
};
|
|
510
526
|
case canonicalToolNames.publishBatchToGroup: {
|
|
511
527
|
const memoryIds = Array.isArray(a.memoryIds) ? a.memoryIds.filter((id) => typeof id === "string") : [];
|
|
512
528
|
return {
|
|
@@ -974,6 +990,17 @@ class EchoMemApiClient {
|
|
|
974
990
|
throw new Error(`get_group_context failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
|
|
975
991
|
}
|
|
976
992
|
}
|
|
993
|
+
async getGroupSessionSharing(args) {
|
|
994
|
+
getGroupSessionSharingSchema.parse(args ?? {});
|
|
995
|
+
const response = await this.axios.get(`/api/extension/social/groups/current/session-sharing?sessionKey=${encodeURIComponent(this.sessionId)}`);
|
|
996
|
+
return response.data;
|
|
997
|
+
}
|
|
998
|
+
async setGroupSessionSharing(args) {
|
|
999
|
+
const parsed = setGroupSessionSharingSchema.parse(args ?? {});
|
|
1000
|
+
const enc = await this.encState();
|
|
1001
|
+
const response = await this.axios.patch("/api/extension/social/groups/current/session-sharing", { ...parsed, sessionKey: this.sessionId }, { headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined });
|
|
1002
|
+
return response.data;
|
|
1003
|
+
}
|
|
977
1004
|
async createGroup(args) {
|
|
978
1005
|
const parsed = createGroupSchema.parse(args ?? {});
|
|
979
1006
|
const response = await this.axios.post("/api/extension/social/groups", parsed);
|
|
@@ -1223,6 +1250,10 @@ class EchoMemMCPServer {
|
|
|
1223
1250
|
return await this.handlePublicMemory(request.params.arguments);
|
|
1224
1251
|
case canonicalToolNames.groupContext:
|
|
1225
1252
|
return await this.handleGroupContext(request.params.arguments);
|
|
1253
|
+
case canonicalToolNames.getGroupSessionSharing:
|
|
1254
|
+
return await this.handleGetGroupSessionSharing(request.params.arguments);
|
|
1255
|
+
case canonicalToolNames.setGroupSessionSharing:
|
|
1256
|
+
return await this.handleSetGroupSessionSharing(request.params.arguments);
|
|
1226
1257
|
case canonicalToolNames.createGroup:
|
|
1227
1258
|
return await this.handleCreateGroup(request.params.arguments);
|
|
1228
1259
|
case canonicalToolNames.createGroupInvite:
|
|
@@ -1404,8 +1435,11 @@ class EchoMemMCPServer {
|
|
|
1404
1435
|
: m.details == null
|
|
1405
1436
|
? ""
|
|
1406
1437
|
: String(m.details).trim();
|
|
1438
|
+
const memoryId = readString(m, "id");
|
|
1407
1439
|
return [
|
|
1408
1440
|
`[${idx + 1}] ${key}${typeof score === "number" ? ` (score ${score.toFixed(3)})` : ""}`,
|
|
1441
|
+
memoryId ? `Memory ID: ${memoryId}` : "",
|
|
1442
|
+
memoryId ? `Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(memoryId), key, description)}` : "",
|
|
1409
1443
|
meta,
|
|
1410
1444
|
`Description: ${description}`,
|
|
1411
1445
|
details ? `Details: ${details}` : "",
|
|
@@ -1423,6 +1457,7 @@ class EchoMemMCPServer {
|
|
|
1423
1457
|
}
|
|
1424
1458
|
const formattedResults = memories
|
|
1425
1459
|
.map((m, idx) => `[Result ${idx + 1}] Memory ID: ${m.id || "unknown"} (Similarity: ${m.similarity_score?.toFixed(3) || "N/A"})
|
|
1460
|
+
Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1426
1461
|
Time: ${m.time} | Location: ${m.location}
|
|
1427
1462
|
Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1428
1463
|
Description: ${m.description}
|
|
@@ -1445,7 +1480,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1445
1480
|
rec.conversation_chars = text.length;
|
|
1446
1481
|
rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
|
|
1447
1482
|
}
|
|
1448
|
-
const { success, memoriesExtracted, memoriesDiscarded, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
|
|
1483
|
+
const { success, memoriesExtracted, memoriesDiscarded, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, groupSessionSharing, groupSync, error, } = await this.client.saveConversation(enrichedArgs);
|
|
1449
1484
|
if (!success)
|
|
1450
1485
|
throw new Error(`EchoMem API Error: ${error}`);
|
|
1451
1486
|
if (rec)
|
|
@@ -1464,6 +1499,24 @@ Details: ${m.details || "N/A"}`)
|
|
|
1464
1499
|
// Surface WHAT was captured (not just the count) so the agent can verify the key facts survived
|
|
1465
1500
|
// extraction, and so it holds the ids to deterministically re-fetch this batch later (warm-up).
|
|
1466
1501
|
const saved = Array.isArray(extractedMemories) ? extractedMemories.filter(isRecord) : [];
|
|
1502
|
+
const sharing = isRecord(groupSessionSharing) ? groupSessionSharing : null;
|
|
1503
|
+
const sync = isRecord(groupSync) ? groupSync : null;
|
|
1504
|
+
const sharingGroup = isRecord(sharing?.group) ? sharing.group : {};
|
|
1505
|
+
const protectedIds = Array.isArray(sync?.protectedMemoryIds) ? sync.protectedMemoryIds : [];
|
|
1506
|
+
const receipt = sharing?.hasGroup !== true
|
|
1507
|
+
? "Saved to your private memory."
|
|
1508
|
+
: sharing?.decision === null || sharing?.decision === undefined
|
|
1509
|
+
? `Saved to your private memory. Ask once: “Share memories saved from this session with ${readString(sharingGroup, "name") ?? "your current group"}?” Then call set_group_session_sharing with the confirmed answer.`
|
|
1510
|
+
: sharing.decision === "private"
|
|
1511
|
+
? "Saved to your private memory."
|
|
1512
|
+
: sync?.synced === true
|
|
1513
|
+
? [
|
|
1514
|
+
"Saved to your private memory and synced to the group.",
|
|
1515
|
+
protectedIds.length
|
|
1516
|
+
? `${protectedIds.length} protected memory item(s) stayed private.`
|
|
1517
|
+
: "",
|
|
1518
|
+
].filter(Boolean).join(" ")
|
|
1519
|
+
: `Saved to your private memory, but group sync failed${readString(sync ?? {}, "error") ? `: ${readString(sync ?? {}, "error")}` : "."}`;
|
|
1467
1520
|
const list = saved
|
|
1468
1521
|
.map((m, idx) => {
|
|
1469
1522
|
const keys = readString(m, "keys") ?? "(no key)";
|
|
@@ -1479,6 +1532,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1479
1532
|
.join("\n\n");
|
|
1480
1533
|
const text = [
|
|
1481
1534
|
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1535
|
+
receipt,
|
|
1482
1536
|
typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
|
|
1483
1537
|
? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
|
|
1484
1538
|
: "",
|
|
@@ -1500,7 +1554,9 @@ Details: ${m.details || "N/A"}`)
|
|
|
1500
1554
|
};
|
|
1501
1555
|
}
|
|
1502
1556
|
const formattedResults = memories
|
|
1503
|
-
.map((m, idx) => `[${idx + 1}]
|
|
1557
|
+
.map((m, idx) => `[${idx + 1}] Memory ID: ${m.id || "unknown"}
|
|
1558
|
+
Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1559
|
+
Time: ${m.time} | Location: ${m.location}
|
|
1504
1560
|
Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1505
1561
|
Description: ${m.description}
|
|
1506
1562
|
Details: ${m.details || "N/A"}`)
|
|
@@ -1526,6 +1582,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1526
1582
|
}
|
|
1527
1583
|
const formattedResults = memories
|
|
1528
1584
|
.map((m, idx) => `[${idx + 1}] ${m.keys || "Saved memory"}${m.id ? ` · id ${m.id}` : ""}
|
|
1585
|
+
${m.id ? `Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id)), m.keys, m.description)}` : ""}
|
|
1529
1586
|
Time: ${m.time} | Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1530
1587
|
Description: ${m.description}
|
|
1531
1588
|
Details: ${m.details || "N/A"}`)
|
|
@@ -1604,7 +1661,9 @@ Details: ${m.details || "N/A"}`)
|
|
|
1604
1661
|
};
|
|
1605
1662
|
}
|
|
1606
1663
|
const formattedResults = memories
|
|
1607
|
-
.map((m, idx) => `[${idx + 1}]
|
|
1664
|
+
.map((m, idx) => `[${idx + 1}] Memory ID: ${m.id || "unknown"}
|
|
1665
|
+
Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1666
|
+
Time: ${m.time} | Keys: ${m.keys || "N/A"}
|
|
1608
1667
|
Location: ${m.location} | Category: ${m.category} | Object: ${m.object}
|
|
1609
1668
|
Description: ${m.description}
|
|
1610
1669
|
Details: ${m.details || "N/A"}`)
|
|
@@ -1657,7 +1716,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1657
1716
|
? m.similarity_score
|
|
1658
1717
|
: undefined;
|
|
1659
1718
|
return `[${idx + 1}] Memory ID: ${m.id || "unknown"}
|
|
1660
|
-
Open memory: ${memoryWebUrl(String(m.id || "unknown"))}
|
|
1719
|
+
Open memory: ${memoryMarkdownLink(memoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1661
1720
|
User ID: ${m.user_id || "unknown"}
|
|
1662
1721
|
User Name: ${m.username || m.user_name || m.name || "Anonymous"}
|
|
1663
1722
|
Time: ${m.time} | Location: ${m.location}
|
|
@@ -1783,7 +1842,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1783
1842
|
}
|
|
1784
1843
|
const text = [
|
|
1785
1844
|
`Memory ID: ${memory.id || parsed.memoryId}`,
|
|
1786
|
-
`Open memory: ${memoryWebUrl(String(memory.id || parsed.memoryId))}`,
|
|
1845
|
+
`Open memory: ${memoryMarkdownLink(memoryWebUrl(String(memory.id || parsed.memoryId)), memory.keys, memory.description)}`,
|
|
1787
1846
|
`Owner User ID: ${memory.owner_user_id || memory.user_id || "Unknown"}`,
|
|
1788
1847
|
memory.time ? `Time: ${memory.time}` : "",
|
|
1789
1848
|
memory.location ? `Location: ${memory.location}` : "",
|
|
@@ -1838,6 +1897,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1838
1897
|
participantText,
|
|
1839
1898
|
"",
|
|
1840
1899
|
"Declared titles and responsibilities are directory facts. Use search_others_memories for current work evidence, and label suggested contribution areas as inference that should be confirmed with the team.",
|
|
1900
|
+
"Use get_group_session_sharing to read this exact session's decision; never infer sharing from the member's role or memories.",
|
|
1841
1901
|
currentParticipant
|
|
1842
1902
|
&& (!readString(currentParticipant, "title") || !readString(currentParticipant, "responsibilitySummary"))
|
|
1843
1903
|
? "Your group profile is incomplete. Use prepare_group_publication to review your memory evidence, propose the missing fields, and save them only after confirmation with update_group_profile."
|
|
@@ -1845,6 +1905,54 @@ Details: ${m.details || "N/A"}`;
|
|
|
1845
1905
|
].filter(Boolean).join("\n");
|
|
1846
1906
|
return { content: [{ type: "text", text }] };
|
|
1847
1907
|
}
|
|
1908
|
+
async handleGetGroupSessionSharing(args) {
|
|
1909
|
+
getGroupSessionSharingSchema.parse(args ?? {});
|
|
1910
|
+
const payload = await this.client.getGroupSessionSharing(args);
|
|
1911
|
+
if (payload?.hasGroup !== true) {
|
|
1912
|
+
return {
|
|
1913
|
+
content: [{
|
|
1914
|
+
type: "text",
|
|
1915
|
+
text: "No company group is configured for this user. Saves remain private; do not ask about session sharing.",
|
|
1916
|
+
}],
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
const group = isRecord(payload?.group) ? payload.group : {};
|
|
1920
|
+
const decision = typeof payload?.decision === "string" ? payload.decision : null;
|
|
1921
|
+
if (!decision) {
|
|
1922
|
+
return {
|
|
1923
|
+
content: [{
|
|
1924
|
+
type: "text",
|
|
1925
|
+
text: `No sharing decision exists for this session. Ask once: “Share memories saved from this session with ${readString(group, "name") ?? "your group"}?” Then call set_group_session_sharing with the explicit Yes/No answer.`,
|
|
1926
|
+
}],
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
content: [{
|
|
1931
|
+
type: "text",
|
|
1932
|
+
text: decision === "share"
|
|
1933
|
+
? `This session is approved for ${readString(group, "name") ?? "the current group"}. Each save persists privately first, then eligible memories sync automatically. Flagged memories stay private.`
|
|
1934
|
+
: "This session is private. Future saves remain private unless the user explicitly changes this session's decision.",
|
|
1935
|
+
}],
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
async handleSetGroupSessionSharing(args) {
|
|
1939
|
+
const parsed = setGroupSessionSharingSchema.parse(args ?? {});
|
|
1940
|
+
const payload = await this.client.setGroupSessionSharing(parsed);
|
|
1941
|
+
const sync = isRecord(payload?.sync) ? payload.sync : null;
|
|
1942
|
+
const protectedIds = Array.isArray(sync?.protectedMemoryIds) ? sync.protectedMemoryIds : [];
|
|
1943
|
+
const group = isRecord(payload?.group) ? payload.group : {};
|
|
1944
|
+
const receipt = parsed.share
|
|
1945
|
+
? sync?.synced === false
|
|
1946
|
+
? "Session sharing is enabled, but the initial group sync failed. Private memories were preserved; retry before claiming publication."
|
|
1947
|
+
: `Session sharing is enabled for ${readString(group, "name") ?? "the current group"}. Existing eligible session memories were synced and later saves will sync automatically.`
|
|
1948
|
+
: "Session sharing is off. Future saves in this session remain private.";
|
|
1949
|
+
return {
|
|
1950
|
+
content: [{
|
|
1951
|
+
type: "text",
|
|
1952
|
+
text: `${receipt}${protectedIds.length ? ` ${protectedIds.length} flagged ${protectedIds.length === 1 ? "memory was" : "memories were"} protected and kept private.` : ""}`,
|
|
1953
|
+
}],
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1848
1956
|
async handlePublishToGroup(args) {
|
|
1849
1957
|
const parsed = publishToGroupSchema.parse(args ?? {});
|
|
1850
1958
|
const payload = await this.client.publishMemoryToGroup(args);
|
|
@@ -1893,8 +2001,8 @@ Details: ${m.details || "N/A"}`;
|
|
|
1893
2001
|
content: [{
|
|
1894
2002
|
type: "text",
|
|
1895
2003
|
text: payload?.alreadyMember
|
|
1896
|
-
? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published. Use prepare_group_publication to review memories and propose any missing title or responsibility fields.`
|
|
1897
|
-
: `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Next use prepare_group_publication to review candidates, infer a proposed title and responsibility summary, and ask the user to confirm that profile together with the publication preview.`,
|
|
2004
|
+
? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published. Use prepare_group_publication to review memories and propose any missing title or responsibility fields, then call get_group_session_sharing and ask once if this session has no decision.`
|
|
2005
|
+
: `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Next use prepare_group_publication to review candidates, infer a proposed title and responsibility summary, and ask the user to confirm that profile together with the publication preview. Also call get_group_session_sharing; if unset, ask once whether memories saved from this session should be shared.`,
|
|
1898
2006
|
}],
|
|
1899
2007
|
};
|
|
1900
2008
|
}
|
|
@@ -1911,7 +2019,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1911
2019
|
: null;
|
|
1912
2020
|
return [
|
|
1913
2021
|
`[${index + 1}] Memory ID: ${readString(candidate, "memoryId") ?? "unknown"}`,
|
|
1914
|
-
`Open memory: ${
|
|
2022
|
+
`Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(readString(candidate, "memoryId") ?? "unknown"), readString(candidate, "keys"), readString(candidate, "description"))}`,
|
|
1915
2023
|
`Created: ${readString(candidate, "createdAt") ?? "unknown"}`,
|
|
1916
2024
|
`Category: ${readString(candidate, "category") ?? "unknown"}`,
|
|
1917
2025
|
`Description: ${readString(candidate, "description") ?? ""}`,
|
|
@@ -1938,9 +2046,9 @@ Details: ${m.details || "N/A"}`;
|
|
|
1938
2046
|
formatted,
|
|
1939
2047
|
"",
|
|
1940
2048
|
flaggedIds.length
|
|
1941
|
-
? `Extra attention required: ${flaggedIds.length} candidate(s) are flagged (${flaggedIds.join(", ")}). Show them in a separate warning and
|
|
2049
|
+
? `Extra attention required: ${flaggedIds.length} candidate(s) are flagged (${flaggedIds.join(", ")}). Nothing has been published. Show them in a separate warning and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories for publication attention. Never auto-flag based on inference. Require separate explicit acknowledgement before including their exact IDs in acknowledgedFlaggedMemoryIds.`
|
|
1942
2050
|
: "",
|
|
1943
|
-
"Select exact memory IDs matching the user's instruction. From memory evidence, draft a concise title and responsibility summary. Show the profile proposal and memory preview together, then ask for explicit confirmation. After confirmation, call update_group_profile and complete_group_publication.",
|
|
2051
|
+
"Select exact memory IDs matching the user's instruction. From memory evidence, draft a concise title and responsibility summary. If an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means you will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag. Show the profile proposal and memory preview together, then ask for explicit confirmation. After confirmation, call update_group_profile and complete_group_publication.",
|
|
1944
2052
|
].join("\n"),
|
|
1945
2053
|
}],
|
|
1946
2054
|
};
|
|
@@ -1958,6 +2066,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1958
2066
|
`Flagged ${payload?.flaggedCount ?? parsed.memoryIds.length} memories for publication attention.`,
|
|
1959
2067
|
`Label: ${parsed.label}`,
|
|
1960
2068
|
`Memory IDs: ${parsed.memoryIds.join(", ")}`,
|
|
2069
|
+
`Private review links:\n${parsed.memoryIds.map((memoryId) => `- ${memoryId}: ${personalMemoryWebUrl(memoryId)}`).join("\n")}`,
|
|
1961
2070
|
"No memory was published, decrypted, or made globally public.",
|
|
1962
2071
|
alreadyPublished > 0
|
|
1963
2072
|
? `Attention: ${alreadyPublished} flagged memories already have group snapshots. This flag does not retract those existing snapshots.`
|
package/dist/package-metadata.js
CHANGED
|
@@ -28,11 +28,14 @@ export const MCP_SERVER_INSTRUCTIONS = [
|
|
|
28
28
|
`If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session.`,
|
|
29
29
|
"Use echomem_update_status to check whether npm has a newer bridge; it is cached and non-blocking during normal tool listing.",
|
|
30
30
|
"Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
|
|
31
|
+
"Before the final response for work that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call save_conversation. This is a private checkpoint and does not require a separate user request. Skip secrets, incidental chatter, and trivial read-only work. If an encrypted vault is locked, tell the user to run echomem-mcp unlock rather than silently skipping the checkpoint.",
|
|
32
|
+
"For a user with a company group, call get_group_session_sharing near session start or after the first qualifying private save. If this exact session has no decision, ask once whether memories saved from the session should be shared with the current group, then store only the explicit Yes/No answer with set_group_session_sharing. A Yes decision syncs eligible memories after each private save; a No decision keeps later saves private. Flagged memories stay private.",
|
|
31
33
|
"For company-group sharing, use get_group_context for orientation; create_memory_group/create_group_invite/join_memory_group for membership; and prepare_group_publication as a no-publication preview.",
|
|
32
34
|
"After joining or when profile fields are missing, use candidate memory evidence to propose a title and responsibility summary. Ask the user to confirm that proposal together with the publication preview, then call update_group_profile and complete_group_publication.",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
35
|
+
"Use one canonical https://echoknows.com/memory/<memory-id> link for private, group, and friend evidence. Label it with the memory key; the site resolves the authorized representation.",
|
|
36
|
+
"Each search result is one memory: preserve its Memory ID and canonical echoknows.com link when citing it.",
|
|
37
|
+
"During a publication preview, if an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. For sensitive-topic flags, search and preview exact owned memories before confirmed flag_memories_for_publication_attention. Separate already-flagged candidates, state that nothing has been published yet, and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories.",
|
|
38
|
+
"Never save an inferred group profile. Manual prepared publication requires explicit preview confirmation; flagged memories still require separate exact-memory confirmation. Never store or log an echo_grp_ invite code.",
|
|
36
39
|
].join(" ");
|
|
37
40
|
export function withMcpVersion(description) {
|
|
38
41
|
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` (or add \`--client cursor|windsurf|claude-desktop|claude-code|codex\` for a single client), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
|