@echomem/mcp 1.4.26 → 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 +111 -16
- package/dist/package-metadata.js +5 -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 +143 -10
- package/dist/v1-contract.js +39 -6
- 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,13 +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/
|
|
21
|
-
const ECHO_PERSONAL_MEMORY_WEB_URL = (process.env.ECHO_PERSONAL_MEMORY_WEB_URL || "https://echoknows.com/memories/timeline").replace(/\/$/, "");
|
|
20
|
+
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
22
21
|
function memoryWebUrl(memoryId) {
|
|
23
|
-
return `${ECHO_MEMORY_WEB_URL}
|
|
22
|
+
return `${ECHO_MEMORY_WEB_URL}/${encodeURIComponent(memoryId)}`;
|
|
24
23
|
}
|
|
25
24
|
function personalMemoryWebUrl(memoryId) {
|
|
26
|
-
return
|
|
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})`;
|
|
27
34
|
}
|
|
28
35
|
/** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
|
|
29
36
|
class NoTokenError extends Error {
|
|
@@ -511,6 +518,11 @@ function inputAnalyticsForTool(canonicalName, args) {
|
|
|
511
518
|
confirmed: a.confirmed === true,
|
|
512
519
|
};
|
|
513
520
|
}
|
|
521
|
+
case canonicalToolNames.setGroupSessionSharing:
|
|
522
|
+
return {
|
|
523
|
+
share_to_group: typeof a.share === "boolean" ? a.share : undefined,
|
|
524
|
+
confirmed: a.confirmed === true,
|
|
525
|
+
};
|
|
514
526
|
case canonicalToolNames.publishBatchToGroup: {
|
|
515
527
|
const memoryIds = Array.isArray(a.memoryIds) ? a.memoryIds.filter((id) => typeof id === "string") : [];
|
|
516
528
|
return {
|
|
@@ -978,6 +990,17 @@ class EchoMemApiClient {
|
|
|
978
990
|
throw new Error(`get_group_context failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
|
|
979
991
|
}
|
|
980
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
|
+
}
|
|
981
1004
|
async createGroup(args) {
|
|
982
1005
|
const parsed = createGroupSchema.parse(args ?? {});
|
|
983
1006
|
const response = await this.axios.post("/api/extension/social/groups", parsed);
|
|
@@ -1227,6 +1250,10 @@ class EchoMemMCPServer {
|
|
|
1227
1250
|
return await this.handlePublicMemory(request.params.arguments);
|
|
1228
1251
|
case canonicalToolNames.groupContext:
|
|
1229
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);
|
|
1230
1257
|
case canonicalToolNames.createGroup:
|
|
1231
1258
|
return await this.handleCreateGroup(request.params.arguments);
|
|
1232
1259
|
case canonicalToolNames.createGroupInvite:
|
|
@@ -1412,7 +1439,7 @@ class EchoMemMCPServer {
|
|
|
1412
1439
|
return [
|
|
1413
1440
|
`[${idx + 1}] ${key}${typeof score === "number" ? ` (score ${score.toFixed(3)})` : ""}`,
|
|
1414
1441
|
memoryId ? `Memory ID: ${memoryId}` : "",
|
|
1415
|
-
memoryId ? `Open private memory: ${personalMemoryWebUrl(memoryId)}` : "",
|
|
1442
|
+
memoryId ? `Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(memoryId), key, description)}` : "",
|
|
1416
1443
|
meta,
|
|
1417
1444
|
`Description: ${description}`,
|
|
1418
1445
|
details ? `Details: ${details}` : "",
|
|
@@ -1430,7 +1457,7 @@ class EchoMemMCPServer {
|
|
|
1430
1457
|
}
|
|
1431
1458
|
const formattedResults = memories
|
|
1432
1459
|
.map((m, idx) => `[Result ${idx + 1}] Memory ID: ${m.id || "unknown"} (Similarity: ${m.similarity_score?.toFixed(3) || "N/A"})
|
|
1433
|
-
Open private memory: ${personalMemoryWebUrl(String(m.id || "unknown"))}
|
|
1460
|
+
Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1434
1461
|
Time: ${m.time} | Location: ${m.location}
|
|
1435
1462
|
Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1436
1463
|
Description: ${m.description}
|
|
@@ -1453,7 +1480,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1453
1480
|
rec.conversation_chars = text.length;
|
|
1454
1481
|
rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
|
|
1455
1482
|
}
|
|
1456
|
-
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);
|
|
1457
1484
|
if (!success)
|
|
1458
1485
|
throw new Error(`EchoMem API Error: ${error}`);
|
|
1459
1486
|
if (rec)
|
|
@@ -1472,6 +1499,24 @@ Details: ${m.details || "N/A"}`)
|
|
|
1472
1499
|
// Surface WHAT was captured (not just the count) so the agent can verify the key facts survived
|
|
1473
1500
|
// extraction, and so it holds the ids to deterministically re-fetch this batch later (warm-up).
|
|
1474
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")}` : "."}`;
|
|
1475
1520
|
const list = saved
|
|
1476
1521
|
.map((m, idx) => {
|
|
1477
1522
|
const keys = readString(m, "keys") ?? "(no key)";
|
|
@@ -1487,6 +1532,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1487
1532
|
.join("\n\n");
|
|
1488
1533
|
const text = [
|
|
1489
1534
|
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1535
|
+
receipt,
|
|
1490
1536
|
typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
|
|
1491
1537
|
? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
|
|
1492
1538
|
: "",
|
|
@@ -1509,7 +1555,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1509
1555
|
}
|
|
1510
1556
|
const formattedResults = memories
|
|
1511
1557
|
.map((m, idx) => `[${idx + 1}] Memory ID: ${m.id || "unknown"}
|
|
1512
|
-
Open private memory: ${personalMemoryWebUrl(String(m.id || "unknown"))}
|
|
1558
|
+
Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1513
1559
|
Time: ${m.time} | Location: ${m.location}
|
|
1514
1560
|
Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1515
1561
|
Description: ${m.description}
|
|
@@ -1536,7 +1582,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1536
1582
|
}
|
|
1537
1583
|
const formattedResults = memories
|
|
1538
1584
|
.map((m, idx) => `[${idx + 1}] ${m.keys || "Saved memory"}${m.id ? ` · id ${m.id}` : ""}
|
|
1539
|
-
${m.id ? `Open private memory: ${personalMemoryWebUrl(String(m.id))}` : ""}
|
|
1585
|
+
${m.id ? `Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id)), m.keys, m.description)}` : ""}
|
|
1540
1586
|
Time: ${m.time} | Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1541
1587
|
Description: ${m.description}
|
|
1542
1588
|
Details: ${m.details || "N/A"}`)
|
|
@@ -1616,7 +1662,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1616
1662
|
}
|
|
1617
1663
|
const formattedResults = memories
|
|
1618
1664
|
.map((m, idx) => `[${idx + 1}] Memory ID: ${m.id || "unknown"}
|
|
1619
|
-
Open private memory: ${personalMemoryWebUrl(String(m.id || "unknown"))}
|
|
1665
|
+
Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1620
1666
|
Time: ${m.time} | Keys: ${m.keys || "N/A"}
|
|
1621
1667
|
Location: ${m.location} | Category: ${m.category} | Object: ${m.object}
|
|
1622
1668
|
Description: ${m.description}
|
|
@@ -1670,7 +1716,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1670
1716
|
? m.similarity_score
|
|
1671
1717
|
: undefined;
|
|
1672
1718
|
return `[${idx + 1}] Memory ID: ${m.id || "unknown"}
|
|
1673
|
-
Open memory: ${memoryWebUrl(String(m.id || "unknown"))}
|
|
1719
|
+
Open memory: ${memoryMarkdownLink(memoryWebUrl(String(m.id || "unknown")), m.keys, m.description)}
|
|
1674
1720
|
User ID: ${m.user_id || "unknown"}
|
|
1675
1721
|
User Name: ${m.username || m.user_name || m.name || "Anonymous"}
|
|
1676
1722
|
Time: ${m.time} | Location: ${m.location}
|
|
@@ -1796,7 +1842,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1796
1842
|
}
|
|
1797
1843
|
const text = [
|
|
1798
1844
|
`Memory ID: ${memory.id || parsed.memoryId}`,
|
|
1799
|
-
`Open memory: ${memoryWebUrl(String(memory.id || parsed.memoryId))}`,
|
|
1845
|
+
`Open memory: ${memoryMarkdownLink(memoryWebUrl(String(memory.id || parsed.memoryId)), memory.keys, memory.description)}`,
|
|
1800
1846
|
`Owner User ID: ${memory.owner_user_id || memory.user_id || "Unknown"}`,
|
|
1801
1847
|
memory.time ? `Time: ${memory.time}` : "",
|
|
1802
1848
|
memory.location ? `Location: ${memory.location}` : "",
|
|
@@ -1851,6 +1897,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1851
1897
|
participantText,
|
|
1852
1898
|
"",
|
|
1853
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.",
|
|
1854
1901
|
currentParticipant
|
|
1855
1902
|
&& (!readString(currentParticipant, "title") || !readString(currentParticipant, "responsibilitySummary"))
|
|
1856
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."
|
|
@@ -1858,6 +1905,54 @@ Details: ${m.details || "N/A"}`;
|
|
|
1858
1905
|
].filter(Boolean).join("\n");
|
|
1859
1906
|
return { content: [{ type: "text", text }] };
|
|
1860
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
|
+
}
|
|
1861
1956
|
async handlePublishToGroup(args) {
|
|
1862
1957
|
const parsed = publishToGroupSchema.parse(args ?? {});
|
|
1863
1958
|
const payload = await this.client.publishMemoryToGroup(args);
|
|
@@ -1906,8 +2001,8 @@ Details: ${m.details || "N/A"}`;
|
|
|
1906
2001
|
content: [{
|
|
1907
2002
|
type: "text",
|
|
1908
2003
|
text: payload?.alreadyMember
|
|
1909
|
-
? `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.`
|
|
1910
|
-
: `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.`,
|
|
1911
2006
|
}],
|
|
1912
2007
|
};
|
|
1913
2008
|
}
|
|
@@ -1924,7 +2019,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1924
2019
|
: null;
|
|
1925
2020
|
return [
|
|
1926
2021
|
`[${index + 1}] Memory ID: ${readString(candidate, "memoryId") ?? "unknown"}`,
|
|
1927
|
-
`Open private memory: ${personalMemoryWebUrl(readString(candidate, "memoryId") ?? "unknown")}`,
|
|
2022
|
+
`Open private memory: ${memoryMarkdownLink(personalMemoryWebUrl(readString(candidate, "memoryId") ?? "unknown"), readString(candidate, "keys"), readString(candidate, "description"))}`,
|
|
1928
2023
|
`Created: ${readString(candidate, "createdAt") ?? "unknown"}`,
|
|
1929
2024
|
`Category: ${readString(candidate, "category") ?? "unknown"}`,
|
|
1930
2025
|
`Description: ${readString(candidate, "description") ?? ""}`,
|
package/dist/package-metadata.js
CHANGED
|
@@ -28,12 +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
|
-
"Use
|
|
34
|
-
"Each
|
|
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.",
|
|
35
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.",
|
|
36
|
-
"Never save an inferred group profile
|
|
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.",
|
|
37
39
|
].join(" ");
|
|
38
40
|
export function withMcpVersion(description) {
|
|
39
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.`;
|