@echomem/mcp 1.4.33 → 1.4.35
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/README.md +12 -45
- package/dist/context-analysis/claude-native-canonical.js +87 -29
- package/dist/forensics.js +12 -9
- package/dist/index.js +177 -24
- package/dist/package-metadata.js +17 -14
- package/dist/save-checkpoint-hook.js +3 -3
- package/dist/setup-page/client-extraction.js +35 -11
- package/dist/setup-page.js +1 -1
- package/dist/setup-preview.js +25 -2
- package/dist/setup.js +136 -81
- package/dist/v1-contract.js +155 -10
- package/package.json +11 -6
- package/templates/codex-skills/echomem-save/SKILL.md +9 -9
- package/templates/echomem-recall.md +9 -9
package/dist/setup.js
CHANGED
|
@@ -31,7 +31,7 @@ import { syncCodexUsage } from "./codex-sync.js";
|
|
|
31
31
|
import { renderSetupPage } from "./setup-page.js";
|
|
32
32
|
import { parseSetupPreviewState } from "./setup-preview.js";
|
|
33
33
|
import { repoLabel, validateForensicReportForSetup } from "./forensics.js";
|
|
34
|
-
import {
|
|
34
|
+
import { installSaveCheckpointHooks } from "./hud/hooks.js";
|
|
35
35
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
36
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
37
37
|
// The setup dashboard, account login, and encryption passphrase entry are all served by this
|
|
@@ -218,13 +218,15 @@ export function knownClients() {
|
|
|
218
218
|
{ id: "codex", label: "Codex", kind: "command", detectDir: codexHome(), configPath: path.join(codexHome(), "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
|
|
219
219
|
];
|
|
220
220
|
}
|
|
221
|
-
/** A client is "present" if its config dir already exists
|
|
221
|
+
/** A client is "present" if its config dir already exists — a cheap, side-effect-free heuristic. */
|
|
222
222
|
export function detectClients() {
|
|
223
223
|
return knownClients().filter((c) => {
|
|
224
224
|
if (c.kind === "json")
|
|
225
225
|
return fs.existsSync(path.dirname(c.configPath));
|
|
226
226
|
if (c.kind === "command")
|
|
227
227
|
return fs.existsSync(c.detectDir);
|
|
228
|
+
if (c.id === "claude-code")
|
|
229
|
+
return fs.existsSync(home(".claude"));
|
|
228
230
|
return false;
|
|
229
231
|
});
|
|
230
232
|
}
|
|
@@ -390,9 +392,9 @@ function echomemGuidanceBlock() {
|
|
|
390
392
|
"- Use EchoMem's `echomem-*` skills and MCP tools as the default memory provider. Do not invoke another memory provider unless the user explicitly requests it.",
|
|
391
393
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
392
394
|
'- If the final user-facing answer materially relies on one or more EchoMem memories, end it with a compact `EchoMem sources:` list containing only the memories actually used. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.',
|
|
393
|
-
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to
|
|
395
|
+
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip it.",
|
|
394
396
|
'- After `save_conversation` succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact `EchoMem saved:` list containing every memory created by that call. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from `EchoMem sources:` and does not imply the newly saved memories informed the answer.',
|
|
395
|
-
"- For a user with a company group, call `
|
|
397
|
+
"- For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep private choice. If the tool returns a text fallback, relay its exact question and call `set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset; never infer an answer. Saves sync eligible memories to every approved group; a No keeps them private for that group.",
|
|
396
398
|
"- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
|
|
397
399
|
"",
|
|
398
400
|
"### Company group memory",
|
|
@@ -401,14 +403,12 @@ function echomemGuidanceBlock() {
|
|
|
401
403
|
"- Group sharing is scoped to an opaque id carried only in the current conversation, not to the MCP transport session. Membership is rechecked for each sync. Flagged memories are withheld from automatic conversation sync and remain private.",
|
|
402
404
|
"- If a user asks to create a group, call `create_memory_group`; if they ask for a code to share, call `create_group_invite` and return the secret invite code only to that user. Never save the invite code to memory or include it in logs, analytics, summaries, or unrelated output.",
|
|
403
405
|
"- If a user supplies an `echo_grp_...` code and explicitly asks to join, call `join_memory_group`. Joining never authorizes publishing by itself and must not move a user out of another group. After joining, continue into the profile-and-publication preview instead of leaving title or responsibility blank.",
|
|
404
|
-
"- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-publication preview. For encrypted accounts, tell the user to
|
|
406
|
+
"- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-publication preview. For encrypted accounts, tell the user to open Echo Desktop and unlock the vault there if the tool reports that the key is required.",
|
|
405
407
|
"- After preparing, select only exact candidate memory IDs that match the user's stated scope and exclude already-published or exact-content duplicates. Use the candidate evidence to draft a concise title and responsibility summary for the current member, but label both as proposals rather than facts.",
|
|
406
408
|
"- Use one canonical evidence link for every memory: preserve the Memory ID and link to `https://echoknows.com/memory/<memory-id>`. The site resolves the authorized representation: an owner is sent to their private timeline, while current group/friend access opens an authorized publication snapshot or public memory. The visible Markdown label should use the memory key, not the raw URL or UUID.",
|
|
407
409
|
"- If the user asks to flag memories about a sensitive topic, search their own memories first, show the exact matches with owner-only personal links, and ask them to confirm. Only then call `flag_memories_for_publication_attention`; flagging does not publish, decrypt, change visibility, or retract an existing group snapshot.",
|
|
408
410
|
"- Present the proposed title/responsibility and the memory publication preview together and ask for explicit confirmation. Never save an inferred profile or publish memories before confirmation. If an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain naturally: 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 based on agent inference. Show already-flagged candidates in a separate warning, state that nothing has been published yet, and offer three choices: exclude them, review them separately, or first search for and mark similar sensitive owned memories for publication attention.",
|
|
409
411
|
"- On confirmation, call `update_group_profile` with the confirmed title, responsibility summary, and `confirmed: true`, then call `complete_group_publication` with the exact `scanId`, selected memory IDs, and `confirmed: true`. If a flagged memory is selected, require separate explicit acknowledgement and pass its exact ID in `acknowledgedFlaggedMemoryIds`. If the user edits either proposal, use their wording. An explicit request to join and upload still requires this preview and confirmation.",
|
|
410
|
-
"- If the user asks to show, reopen, restart, or bring back the EchoMem HUD (the context-health overlay), run the shell command `echomem-hud app --client auto`.",
|
|
411
|
-
"- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
|
|
412
412
|
AGENTS_MD_END,
|
|
413
413
|
].join("\n");
|
|
414
414
|
}
|
|
@@ -1310,20 +1310,99 @@ function forensicStageLabel(stage) {
|
|
|
1310
1310
|
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
1311
1311
|
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
1312
1312
|
export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
1313
|
+
let lastProgress = null;
|
|
1314
|
+
const recordProgress = (progress) => {
|
|
1315
|
+
lastProgress = progress;
|
|
1316
|
+
onProgress?.(progress);
|
|
1317
|
+
};
|
|
1318
|
+
return runForensicReportWorker(recordProgress, options).catch(async (primaryError) => {
|
|
1319
|
+
if (options.failOpen === false)
|
|
1320
|
+
throw primaryError;
|
|
1321
|
+
const failureCode = errorCode(primaryError) || "REPORT_BUILD_FAILED";
|
|
1322
|
+
console.error(`[echomem] local scan degraded after ${failureCode}; continuing without local-history analysis`);
|
|
1323
|
+
onProgress?.({
|
|
1324
|
+
done: lastProgress?.done || 0,
|
|
1325
|
+
total: lastProgress?.total || 0,
|
|
1326
|
+
stage: "finalizing-report",
|
|
1327
|
+
detail: "finishing setup without optional local-history analysis",
|
|
1328
|
+
overall: 0.99,
|
|
1329
|
+
stageDone: 0,
|
|
1330
|
+
stageTotal: 0,
|
|
1331
|
+
});
|
|
1332
|
+
return runForensicReportWorker(undefined, {
|
|
1333
|
+
timeoutMs: 30_000,
|
|
1334
|
+
maxOldGenerationSizeMb: Math.max(64, options.maxOldGenerationSizeMb || 0),
|
|
1335
|
+
}, [], failureCode);
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
function errorCode(error) {
|
|
1339
|
+
return error && typeof error === "object" && "code" in error
|
|
1340
|
+
? String(error.code || "")
|
|
1341
|
+
: "";
|
|
1342
|
+
}
|
|
1343
|
+
/** Turn an optional onboarding-stats failure into a terminal, non-polling payload.
|
|
1344
|
+
*
|
|
1345
|
+
* This must only be used for dashboard enrichment. Authentication, consent, vault access, and an
|
|
1346
|
+
* import the user explicitly started retain their normal hard-failure behavior. The reason is a
|
|
1347
|
+
* stable internal code rather than an exception message, so local paths or conversation details
|
|
1348
|
+
* can never cross the localhost bridge by accident. */
|
|
1349
|
+
export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
1350
|
+
const fallback = {
|
|
1351
|
+
schemaVersion: 1,
|
|
1352
|
+
generatedFrom: ["~/.codex/sessions", "~/.claude/projects"],
|
|
1353
|
+
llmCallsUsed: 0,
|
|
1354
|
+
transcriptsUploaded: false,
|
|
1355
|
+
sessions: { total: 0, codex: 0, claudeCode: 0 },
|
|
1356
|
+
migratable: { pending: 0, alreadyMigrated: 0 },
|
|
1357
|
+
memoriesCaptured: null,
|
|
1358
|
+
};
|
|
1359
|
+
const completed = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
1360
|
+
? { ...payload }
|
|
1361
|
+
: fallback;
|
|
1362
|
+
delete completed.partial;
|
|
1363
|
+
completed.optionalDiagnostics = {
|
|
1364
|
+
degraded: true,
|
|
1365
|
+
reason: /^[A-Z0-9_]+$/.test(reason) ? reason : "OPTIONAL_STATS_FAILED",
|
|
1366
|
+
countsTrusted,
|
|
1367
|
+
};
|
|
1368
|
+
return completed;
|
|
1369
|
+
}
|
|
1370
|
+
function runForensicReportWorker(onProgress, options, sources, degradedReason) {
|
|
1313
1371
|
const forensicsUrl = runtimeModuleUrl("forensics");
|
|
1372
|
+
const serializedSources = sources === undefined ? "undefined" : JSON.stringify(sources);
|
|
1373
|
+
const serializedDegradedReason = JSON.stringify(degradedReason || "");
|
|
1314
1374
|
const code = `
|
|
1315
1375
|
import { parentPort } from "node:worker_threads";
|
|
1316
|
-
import { buildForensicReport } from ${JSON.stringify(forensicsUrl)};
|
|
1376
|
+
import { buildForensicReport, validateForensicReportForSetup } from ${JSON.stringify(forensicsUrl)};
|
|
1317
1377
|
try {
|
|
1318
1378
|
const report = await buildForensicReport({
|
|
1379
|
+
sources: ${serializedSources},
|
|
1319
1380
|
includeLegacyGoldenStandard: false,
|
|
1320
1381
|
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1321
1382
|
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1322
1383
|
}),
|
|
1323
1384
|
});
|
|
1385
|
+
const degradedReason = ${serializedDegradedReason};
|
|
1386
|
+
if (degradedReason) {
|
|
1387
|
+
report.scanDiagnostics = {
|
|
1388
|
+
degraded: true,
|
|
1389
|
+
reason: degradedReason,
|
|
1390
|
+
skippedSources: ["codex", "claude"],
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const validation = validateForensicReportForSetup(report);
|
|
1394
|
+
if (!validation.ok) {
|
|
1395
|
+
const error = new Error(validation.message);
|
|
1396
|
+
error.code = validation.code;
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1324
1399
|
parentPort?.postMessage({ ok: true, report });
|
|
1325
1400
|
} catch (error) {
|
|
1326
|
-
parentPort?.postMessage({
|
|
1401
|
+
parentPort?.postMessage({
|
|
1402
|
+
ok: false,
|
|
1403
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1404
|
+
code: error && typeof error === "object" && "code" in error ? String(error.code || "") : "",
|
|
1405
|
+
});
|
|
1327
1406
|
}
|
|
1328
1407
|
`;
|
|
1329
1408
|
const requestedHeapMb = options.maxOldGenerationSizeMb;
|
|
@@ -1367,7 +1446,10 @@ export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
|
1367
1446
|
finish({ ok: true, report: msg.report });
|
|
1368
1447
|
return;
|
|
1369
1448
|
}
|
|
1370
|
-
|
|
1449
|
+
const error = new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed");
|
|
1450
|
+
if (typeof msg.code === "string" && msg.code)
|
|
1451
|
+
error.code = msg.code;
|
|
1452
|
+
finish({ ok: false, error });
|
|
1371
1453
|
});
|
|
1372
1454
|
worker.once("error", (error) => {
|
|
1373
1455
|
finish({ ok: false, error });
|
|
@@ -2620,19 +2702,20 @@ async function cmdSetup(flags) {
|
|
|
2620
2702
|
else {
|
|
2621
2703
|
await cmdLogin(flags);
|
|
2622
2704
|
}
|
|
2623
|
-
if (flags["with-hud"])
|
|
2624
|
-
|
|
2705
|
+
if (flags["with-hud"]) {
|
|
2706
|
+
console.log("ℹ️ The standalone EchoMem HUD has been retired. Echo Desktop now owns setup and status.");
|
|
2707
|
+
}
|
|
2625
2708
|
}
|
|
2626
2709
|
/**
|
|
2627
2710
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
2628
2711
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
2629
|
-
* Codex skills
|
|
2712
|
+
* Codex skills and writes the AGENTS.md memory guidance. One browser
|
|
2630
2713
|
* bridge then runs permission → report → login → plan if needed → extraction in that order.
|
|
2631
2714
|
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
2632
2715
|
*/
|
|
2633
2716
|
async function cmdInit(flags) {
|
|
2634
|
-
console.log("Setting up EchoMem — shared memory for all your coding agents
|
|
2635
|
-
// 1. Configure every installed agent + write AGENTS.md. Hold login
|
|
2717
|
+
console.log("Setting up EchoMem — shared memory for all your coding agents.\n");
|
|
2718
|
+
// 1. Configure every installed agent + write AGENTS.md. Hold login so we control ordering.
|
|
2636
2719
|
await cmdSetup({
|
|
2637
2720
|
...flags,
|
|
2638
2721
|
all: true,
|
|
@@ -2641,10 +2724,7 @@ async function cmdInit(flags) {
|
|
|
2641
2724
|
"init-quiet": true,
|
|
2642
2725
|
"install-save-hooks": flags["no-save-hooks"] !== true,
|
|
2643
2726
|
});
|
|
2644
|
-
// 2.
|
|
2645
|
-
if (!flags["no-hud"])
|
|
2646
|
-
await cmdSetupHud(flags);
|
|
2647
|
-
// 3. Start one ordered onboarding bridge. A fresh device logs in only after consent + report.
|
|
2727
|
+
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent + report.
|
|
2648
2728
|
console.log("");
|
|
2649
2729
|
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
2650
2730
|
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
@@ -2653,13 +2733,7 @@ async function cmdInit(flags) {
|
|
|
2653
2733
|
console.log("");
|
|
2654
2734
|
console.log("🎉 EchoMem is ready.");
|
|
2655
2735
|
console.log(" • MCP memory is configured for every coding agent installed on this machine.");
|
|
2656
|
-
|
|
2657
|
-
console.log(' • The context HUD is running (top-right). Right-click it → "Show after restart" to keep it,');
|
|
2658
|
-
console.log(' or just tell your agent "open the EchoMem HUD" anytime (it runs: echomem-hud app).');
|
|
2659
|
-
}
|
|
2660
|
-
else {
|
|
2661
|
-
console.log(' • Start the context HUD anytime with: echomem-hud app');
|
|
2662
|
-
}
|
|
2736
|
+
console.log(" • Echo Desktop shows connection status and manages this device credential.");
|
|
2663
2737
|
console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
|
|
2664
2738
|
}
|
|
2665
2739
|
/**
|
|
@@ -2742,55 +2816,6 @@ function selectSetupTargets(requested, all) {
|
|
|
2742
2816
|
}
|
|
2743
2817
|
return requested ? knownClients().filter((client) => client.id === requested) : detectClients();
|
|
2744
2818
|
}
|
|
2745
|
-
async function cmdSetupHud(flags) {
|
|
2746
|
-
const client = parseHudClient(flags["hud-client"] || "auto");
|
|
2747
|
-
const hudCli = resolveHudCliPath();
|
|
2748
|
-
console.log("");
|
|
2749
|
-
console.log(`✅ EchoMem HUD available: ${process.execPath} ${hudCli}`);
|
|
2750
|
-
if (flags["install-hud-hooks"]) {
|
|
2751
|
-
const paths = installHooks(client === "claude-desktop" ? "auto" : client);
|
|
2752
|
-
console.log(`✅ Installed EchoMem HUD hook support:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
|
|
2753
|
-
console.log(" Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
|
|
2754
|
-
}
|
|
2755
|
-
else {
|
|
2756
|
-
console.log("ℹ️ HUD hooks not installed. Add --install-hud-hooks if you want lifecycle wakeups.");
|
|
2757
|
-
}
|
|
2758
|
-
if (!flags["no-launch-hud"]) {
|
|
2759
|
-
try {
|
|
2760
|
-
spawn(process.execPath, [hudCli, "app", "--client", client], { stdio: "ignore", detached: true }).unref();
|
|
2761
|
-
console.log("✅ Launched EchoMem HUD app.");
|
|
2762
|
-
}
|
|
2763
|
-
catch {
|
|
2764
|
-
console.log(`ℹ️ Could not auto-launch HUD. Run: echomem-hud app --client ${client}`);
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
else {
|
|
2768
|
-
console.log(`Run the HUD later with: echomem-hud app --client ${client}`);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
function resolveHudCliPath() {
|
|
2772
|
-
const entry = fs.realpathSync(process.argv[1] || "");
|
|
2773
|
-
const compiledEntry = compiledDistPathForSource(entry);
|
|
2774
|
-
if (compiledEntry) {
|
|
2775
|
-
const compiledHud = path.join(path.dirname(compiledEntry), "hud", "cli.js");
|
|
2776
|
-
if (fs.existsSync(compiledHud))
|
|
2777
|
-
return compiledHud;
|
|
2778
|
-
}
|
|
2779
|
-
const base = path.dirname(entry);
|
|
2780
|
-
const candidate = path.join(base, "hud", "cli.js");
|
|
2781
|
-
if (fs.existsSync(candidate))
|
|
2782
|
-
return candidate;
|
|
2783
|
-
const sourceCandidate = path.join(base, "hud", "cli.ts");
|
|
2784
|
-
if (fs.existsSync(sourceCandidate)) {
|
|
2785
|
-
throw new Error("The local HUD CLI is not built. Run npm --prefix packages/mcp-server run build and retry.");
|
|
2786
|
-
}
|
|
2787
|
-
return candidate;
|
|
2788
|
-
}
|
|
2789
|
-
function parseHudClient(value) {
|
|
2790
|
-
return value === "codex" || value === "claude-code" || value === "claude-desktop" || value === "both" || value === "auto"
|
|
2791
|
-
? value
|
|
2792
|
-
: "auto";
|
|
2793
|
-
}
|
|
2794
2819
|
function localBridgeOptions(flags) {
|
|
2795
2820
|
const port = typeof flags["dev-port"] === "string" ? Number(flags["dev-port"]) : undefined;
|
|
2796
2821
|
if (port !== undefined && (!Number.isInteger(port) || port < 1024 || port > 65535)) {
|
|
@@ -3063,7 +3088,23 @@ async function cmdOnboarding(flags) {
|
|
|
3063
3088
|
extracted: 0,
|
|
3064
3089
|
});
|
|
3065
3090
|
};
|
|
3066
|
-
const
|
|
3091
|
+
const publishOptionalStatsFallback = (generation, reason, countsTrusted) => {
|
|
3092
|
+
if (generation !== refreshGeneration)
|
|
3093
|
+
return;
|
|
3094
|
+
stats = completeOptionalStatsPayload(stats, reason, countsTrusted);
|
|
3095
|
+
srv.setStats(stats);
|
|
3096
|
+
const pending = countsTrusted ? latestPendingEstimate : 0;
|
|
3097
|
+
srv.setProgress({
|
|
3098
|
+
status: "idle",
|
|
3099
|
+
total: pending,
|
|
3100
|
+
completed: 0,
|
|
3101
|
+
running: 0,
|
|
3102
|
+
queued: pending,
|
|
3103
|
+
failed: 0,
|
|
3104
|
+
extracted: 0,
|
|
3105
|
+
});
|
|
3106
|
+
};
|
|
3107
|
+
const refreshLocalStatsForTokenCore = async (activeToken) => {
|
|
3067
3108
|
const generation = ++refreshGeneration;
|
|
3068
3109
|
lastProcessedImportKeys = null; // shared with /migrate so it can assemble only the pending sessions
|
|
3069
3110
|
let importStatusUnavailable = false;
|
|
@@ -3268,15 +3309,31 @@ async function cmdOnboarding(flags) {
|
|
|
3268
3309
|
return;
|
|
3269
3310
|
stats = fullPayload;
|
|
3270
3311
|
srv.setStats(fullPayload);
|
|
3271
|
-
})()
|
|
3312
|
+
})().catch((e) => {
|
|
3313
|
+
if (generation !== refreshGeneration)
|
|
3314
|
+
return;
|
|
3315
|
+
console.error(`[echomem] optional full local-history stats unavailable; continuing (${errorCode(e) || "FULL_STATS_FAILED"})`);
|
|
3316
|
+
publishOptionalStatsFallback(generation, "FULL_STATS_FAILED", true);
|
|
3317
|
+
});
|
|
3272
3318
|
return initialExact;
|
|
3273
3319
|
}).catch((e) => {
|
|
3274
3320
|
if (generation === refreshGeneration) {
|
|
3275
|
-
console.error(`
|
|
3321
|
+
console.error(`[echomem] optional exact local-history discovery unavailable; continuing (${errorCode(e) || "EXACT_DISCOVERY_FAILED"})`);
|
|
3322
|
+
publishOptionalStatsFallback(generation, "EXACT_DISCOVERY_FAILED", false);
|
|
3276
3323
|
}
|
|
3277
3324
|
return disc;
|
|
3278
3325
|
});
|
|
3279
3326
|
};
|
|
3327
|
+
const refreshLocalStatsForToken = async (activeToken) => {
|
|
3328
|
+
const expectedGeneration = refreshGeneration + 1;
|
|
3329
|
+
try {
|
|
3330
|
+
await refreshLocalStatsForTokenCore(activeToken);
|
|
3331
|
+
}
|
|
3332
|
+
catch (e) {
|
|
3333
|
+
console.error(`[echomem] optional local-history stats unavailable; continuing (${errorCode(e) || "OPTIONAL_STATS_FAILED"})`);
|
|
3334
|
+
publishOptionalStatsFallback(expectedGeneration, "OPTIONAL_STATS_FAILED", false);
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3280
3337
|
srv.setLogoutHandler(resetLocalLoginState);
|
|
3281
3338
|
srv.setTokenRefreshHandler(async ({ token: nextToken, key: nextKey }) => {
|
|
3282
3339
|
if (!await verifyAndPrint({ token: nextToken, key: nextKey }))
|
|
@@ -3712,14 +3769,13 @@ function cmdLogout() {
|
|
|
3712
3769
|
const HELP = `EchoMem MCP — local memory bridge
|
|
3713
3770
|
|
|
3714
3771
|
Usage:
|
|
3715
|
-
echomem-mcp init
|
|
3772
|
+
echomem-mcp init Legacy/headless setup: configure agents + login + local-history onboarding
|
|
3716
3773
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
3717
3774
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
3718
3775
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
3719
3776
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
3720
3777
|
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
3721
3778
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
3722
|
-
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
3723
3779
|
echomem-mcp login Connect this device only; never scans or imports local history
|
|
3724
3780
|
echomem-mcp login --force Reconnect this device with a different account
|
|
3725
3781
|
echomem-mcp unlock Privately unlock the vault on this trusted device
|
|
@@ -3742,7 +3798,6 @@ Manual / headless:
|
|
|
3742
3798
|
${MCP_UPDATE_ALL_COMMAND} # one-shot latest update for detected clients, no browser login
|
|
3743
3799
|
${MCP_UPDATE_COMMAND} --client codex # update one client only
|
|
3744
3800
|
echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
|
|
3745
|
-
echomem-mcp setup --with-hud --install-hud-hooks --install-save-hooks --client codex [--hud-client auto]
|
|
3746
3801
|
echomem-mcp setup --install-save-hooks --all Install proactive private-save completion checks
|
|
3747
3802
|
echomem-mcp sync-usage --days 7 --limit 50 --dry-run
|
|
3748
3803
|
|
package/dist/v1-contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, withMcpVersion, } from "./package-metadata.js";
|
|
2
|
+
import { MCP_DESKTOP_MANAGED, MCP_VAULT_UNLOCK_INSTRUCTION, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, withMcpVersion, } from "./package-metadata.js";
|
|
3
3
|
export const canonicalToolNames = {
|
|
4
4
|
search: "search_memories",
|
|
5
5
|
save: "save_conversation",
|
|
@@ -13,6 +13,7 @@ export const canonicalToolNames = {
|
|
|
13
13
|
recordCitations: "record_memory_citations",
|
|
14
14
|
groupContext: "get_group_context",
|
|
15
15
|
getGroupSessionSharing: "get_group_session_sharing",
|
|
16
|
+
requestGroupSessionSharing: "request_group_session_sharing",
|
|
16
17
|
setGroupSessionSharing: "set_group_session_sharing",
|
|
17
18
|
createGroup: "create_memory_group",
|
|
18
19
|
createGroupInvite: "create_group_invite",
|
|
@@ -38,6 +39,127 @@ export const legacyAliasToCanonical = {
|
|
|
38
39
|
export function resolveCanonicalToolName(toolName) {
|
|
39
40
|
return legacyAliasToCanonical[toolName] ?? toolName;
|
|
40
41
|
}
|
|
42
|
+
export const TOOL_DESCRIPTION_BYTE_LIMIT = 2_000;
|
|
43
|
+
const READ_ONLY_TOOL_NAMES = new Set([
|
|
44
|
+
canonicalToolNames.search,
|
|
45
|
+
"search_memories_by_description_semantic",
|
|
46
|
+
canonicalToolNames.timeRange,
|
|
47
|
+
"search_memories_by_time_range",
|
|
48
|
+
canonicalToolNames.keywords,
|
|
49
|
+
canonicalToolNames.friends,
|
|
50
|
+
canonicalToolNames.searchUsers,
|
|
51
|
+
canonicalToolNames.others,
|
|
52
|
+
canonicalToolNames.publicMemory,
|
|
53
|
+
canonicalToolNames.groupContext,
|
|
54
|
+
canonicalToolNames.getGroupSessionSharing,
|
|
55
|
+
canonicalToolNames.getByContext,
|
|
56
|
+
canonicalToolNames.checkpointByContext,
|
|
57
|
+
canonicalToolNames.report,
|
|
58
|
+
canonicalToolNames.updateStatus,
|
|
59
|
+
canonicalToolNames.contextHealth,
|
|
60
|
+
canonicalToolNames.recompose,
|
|
61
|
+
]);
|
|
62
|
+
const IDEMPOTENT_WRITE_TOOL_NAMES = new Set([
|
|
63
|
+
canonicalToolNames.recordCitations,
|
|
64
|
+
canonicalToolNames.requestGroupSessionSharing,
|
|
65
|
+
canonicalToolNames.setGroupSessionSharing,
|
|
66
|
+
canonicalToolNames.updateGroupProfile,
|
|
67
|
+
canonicalToolNames.completeGroupPublication,
|
|
68
|
+
canonicalToolNames.publishToGroup,
|
|
69
|
+
canonicalToolNames.publishBatchToGroup,
|
|
70
|
+
]);
|
|
71
|
+
const REQUIRES_USER_INTERACTION_TOOL_NAMES = new Set([
|
|
72
|
+
canonicalToolNames.requestGroupSessionSharing,
|
|
73
|
+
canonicalToolNames.setGroupSessionSharing,
|
|
74
|
+
canonicalToolNames.flagPublicationAttention,
|
|
75
|
+
canonicalToolNames.updateGroupProfile,
|
|
76
|
+
canonicalToolNames.completeGroupPublication,
|
|
77
|
+
canonicalToolNames.publishToGroup,
|
|
78
|
+
canonicalToolNames.publishBatchToGroup,
|
|
79
|
+
canonicalToolNames.delete,
|
|
80
|
+
]);
|
|
81
|
+
const ALWAYS_LOAD_TOOL_NAMES = new Set([
|
|
82
|
+
canonicalToolNames.search,
|
|
83
|
+
canonicalToolNames.save,
|
|
84
|
+
]);
|
|
85
|
+
const LOCAL_ONLY_TOOL_NAMES = new Set([
|
|
86
|
+
canonicalToolNames.report,
|
|
87
|
+
canonicalToolNames.contextHealth,
|
|
88
|
+
canonicalToolNames.recompose,
|
|
89
|
+
]);
|
|
90
|
+
function truncateToByteBudget(value, limit) {
|
|
91
|
+
if (Buffer.byteLength(value, "utf8") <= limit)
|
|
92
|
+
return value;
|
|
93
|
+
const ellipsis = "…";
|
|
94
|
+
const contentBudget = limit - Buffer.byteLength(ellipsis, "utf8");
|
|
95
|
+
if (contentBudget <= 0)
|
|
96
|
+
return "";
|
|
97
|
+
let low = 0;
|
|
98
|
+
let high = value.length;
|
|
99
|
+
while (low < high) {
|
|
100
|
+
const middle = Math.ceil((low + high) / 2);
|
|
101
|
+
if (Buffer.byteLength(value.slice(0, middle), "utf8") <= contentBudget)
|
|
102
|
+
low = middle;
|
|
103
|
+
else
|
|
104
|
+
high = middle - 1;
|
|
105
|
+
}
|
|
106
|
+
const safeEnd = low > 0
|
|
107
|
+
&& /[\uD800-\uDBFF]/.test(value.charAt(low - 1))
|
|
108
|
+
&& /[\uDC00-\uDFFF]/.test(value.charAt(low))
|
|
109
|
+
? low - 1
|
|
110
|
+
: low;
|
|
111
|
+
const candidate = value.slice(0, safeEnd);
|
|
112
|
+
const boundary = Math.max(candidate.lastIndexOf("\n"), candidate.lastIndexOf(" "));
|
|
113
|
+
const body = boundary >= Math.floor(candidate.length * 0.8) ? candidate.slice(0, boundary) : candidate;
|
|
114
|
+
return `${body.trimEnd()}${ellipsis}`;
|
|
115
|
+
}
|
|
116
|
+
function boundToolDescription(description) {
|
|
117
|
+
if (Buffer.byteLength(description, "utf8") <= TOOL_DESCRIPTION_BYTE_LIMIT)
|
|
118
|
+
return description;
|
|
119
|
+
const versionMarker = "\n\nEchoMem MCP bridge:";
|
|
120
|
+
const versionIndex = description.lastIndexOf(versionMarker);
|
|
121
|
+
if (versionIndex < 0) {
|
|
122
|
+
return truncateToByteBudget(description, TOOL_DESCRIPTION_BYTE_LIMIT);
|
|
123
|
+
}
|
|
124
|
+
const suffix = description.slice(versionIndex + 2);
|
|
125
|
+
const separator = "\n\n";
|
|
126
|
+
const bodyLimit = TOOL_DESCRIPTION_BYTE_LIMIT
|
|
127
|
+
- Buffer.byteLength(suffix, "utf8")
|
|
128
|
+
- Buffer.byteLength(separator, "utf8");
|
|
129
|
+
if (bodyLimit <= 0)
|
|
130
|
+
return truncateToByteBudget(suffix, TOOL_DESCRIPTION_BYTE_LIMIT);
|
|
131
|
+
return `${truncateToByteBudget(description.slice(0, versionIndex), bodyLimit)}${separator}${suffix}`;
|
|
132
|
+
}
|
|
133
|
+
function compactGroupRoster(groupMap) {
|
|
134
|
+
const labels = groupMap
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((line) => line.trim().replace(/^-\s*/, ""))
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.map((line) => line.split(/\s+—\s+/u, 1)[0] ?? line)
|
|
139
|
+
.map((label) => truncateToByteBudget(label, 48));
|
|
140
|
+
return labels.length ? `Team roster: ${labels.join("; ")}.` : "";
|
|
141
|
+
}
|
|
142
|
+
function decorateLocalToolSpec(tool) {
|
|
143
|
+
const readOnly = READ_ONLY_TOOL_NAMES.has(tool.name);
|
|
144
|
+
const decorated = {
|
|
145
|
+
...tool,
|
|
146
|
+
description: boundToolDescription(tool.description),
|
|
147
|
+
annotations: {
|
|
148
|
+
readOnlyHint: readOnly,
|
|
149
|
+
destructiveHint: tool.name === canonicalToolNames.delete,
|
|
150
|
+
idempotentHint: readOnly || IDEMPOTENT_WRITE_TOOL_NAMES.has(tool.name),
|
|
151
|
+
openWorldHint: !LOCAL_ONLY_TOOL_NAMES.has(tool.name),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const meta = {};
|
|
155
|
+
if (ALWAYS_LOAD_TOOL_NAMES.has(tool.name)) {
|
|
156
|
+
meta["anthropic/alwaysLoad"] = true;
|
|
157
|
+
}
|
|
158
|
+
if (REQUIRES_USER_INTERACTION_TOOL_NAMES.has(tool.name)) {
|
|
159
|
+
meta["anthropic/requiresUserInteraction"] = true;
|
|
160
|
+
}
|
|
161
|
+
return Object.keys(meta).length > 0 ? { ...decorated, _meta: meta } : decorated;
|
|
162
|
+
}
|
|
41
163
|
const triggerMetadataSchema = {
|
|
42
164
|
triggerMessage: z.string().optional(),
|
|
43
165
|
triggerMessageRole: z.string().optional(),
|
|
@@ -129,6 +251,7 @@ export const getGroupSessionSharingSchema = z.object({
|
|
|
129
251
|
groupSharingScopeId: z.string().uuid().optional(),
|
|
130
252
|
groupId: z.string().uuid().optional(),
|
|
131
253
|
});
|
|
254
|
+
export const requestGroupSessionSharingSchema = getGroupSessionSharingSchema;
|
|
132
255
|
export const setGroupSessionSharingSchema = z.object({
|
|
133
256
|
...triggerMetadataSchema,
|
|
134
257
|
groupSharingScopeId: z.string().uuid(),
|
|
@@ -224,9 +347,9 @@ export function listToolSpecs(opts = {}) {
|
|
|
224
347
|
// Same device as the personal map, aimed at the group surface: the agent judges whether teammates
|
|
225
348
|
// have covered the topic before searching, instead of never calling the group tools at all.
|
|
226
349
|
const groupMapSection = groupMap
|
|
227
|
-
? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${groupMap}\n`
|
|
350
|
+
? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${compactGroupRoster(groupMap)}\n${groupMap}\n`
|
|
228
351
|
: "";
|
|
229
|
-
|
|
352
|
+
const tools = [
|
|
230
353
|
{
|
|
231
354
|
name: canonicalToolNames.search,
|
|
232
355
|
description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
|
|
@@ -265,7 +388,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
265
388
|
},
|
|
266
389
|
{
|
|
267
390
|
name: canonicalToolNames.save,
|
|
268
|
-
description: `Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to
|
|
391
|
+
description: `Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to ${MCP_VAULT_UNLOCK_INSTRUCTION} and never silently skip a qualifying checkpoint. Private persistence happens first. For group sharing, reuse the exact groupSharingScopeId returned by get_group_session_sharing, request_group_session_sharing, or an earlier save in this conversation. Never reuse it in another conversation or save it as memory. Each group has an independent decision under the same conversation scope; eligible memories sync automatically to every approved group, while flagged memories stay private. If a selected group has no decision yet, call request_group_session_sharing so supported hosts render a choice UI; its fallback tells you when a text Yes/No prompt is required. Silence leaves consent unset; never infer the answer. New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule. ${SAVED_MEMORY_RECEIPT_INSTRUCTION}`,
|
|
269
392
|
inputSchema: {
|
|
270
393
|
type: "object",
|
|
271
394
|
properties: {
|
|
@@ -513,7 +636,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
513
636
|
},
|
|
514
637
|
{
|
|
515
638
|
name: canonicalToolNames.getGroupSessionSharing,
|
|
516
|
-
description: "Read the confirmed sharing decision for one group in this exact conversation. On the first call, omit groupSharingScopeId; EchoMem returns a new opaque scope. Reuse that scope only in this conversation. If the user belongs to multiple groups and groupId is omitted, EchoMem returns the available groups
|
|
639
|
+
description: "Read the confirmed sharing decision for one group in this exact conversation. On the first call, omit groupSharingScopeId; EchoMem returns a new opaque scope. Reuse that scope only in this conversation. If the user belongs to multiple groups and groupId is omitted, EchoMem returns the available groups. If a decision is unset, call request_group_session_sharing with this scope and the selected groupId so supported hosts render a choice UI. Silence leaves that group's state unset; never infer the answer.",
|
|
517
640
|
inputSchema: {
|
|
518
641
|
type: "object",
|
|
519
642
|
properties: {
|
|
@@ -532,6 +655,27 @@ export function listToolSpecs(opts = {}) {
|
|
|
532
655
|
},
|
|
533
656
|
},
|
|
534
657
|
},
|
|
658
|
+
{
|
|
659
|
+
name: canonicalToolNames.requestGroupSessionSharing,
|
|
660
|
+
description: "Check one group's sharing decision for this exact conversation and, only when it is unset, ask the user through the MCP client's native choice UI. Omit groupSharingScopeId only on the first call so EchoMem can mint a fresh scope; reuse the returned scope only in this conversation. When multiple groups are returned, call again with the same scope and one groupId at a time. An accepted Share choice backfills eligible private memories and syncs later saves; Keep private records No. Decline or cancel records nothing. If the client cannot render MCP elicitation, relay the returned fallback question and call set_group_session_sharing only after the user explicitly answers.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
groupSharingScopeId: {
|
|
665
|
+
type: "string",
|
|
666
|
+
format: "uuid",
|
|
667
|
+
description: "Reuse the opaque scope returned in this conversation. Omit only on the first sharing check.",
|
|
668
|
+
},
|
|
669
|
+
groupId: {
|
|
670
|
+
type: "string",
|
|
671
|
+
format: "uuid",
|
|
672
|
+
description: "Group to confirm. Required when the user belongs to multiple groups.",
|
|
673
|
+
},
|
|
674
|
+
triggerMessage: { type: "string" },
|
|
675
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
535
679
|
{
|
|
536
680
|
name: canonicalToolNames.setGroupSessionSharing,
|
|
537
681
|
description: "Store the user's explicit Yes/No sharing decision for one group in this exact conversation scope. Pass the exact groupSharingScopeId returned in this conversation and the selected groupId when the user belongs to multiple groups. EchoMem validates membership server-side. Decisions are independent per group: share=true backfills that group and later saves sync to every approved group; share=false keeps this conversation private for that group. Flagged memories remain private.",
|
|
@@ -574,7 +718,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
574
718
|
},
|
|
575
719
|
{
|
|
576
720
|
name: canonicalToolNames.joinGroup,
|
|
577
|
-
description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call
|
|
721
|
+
description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call request_group_session_sharing without a scope so supported hosts render a native sharing choice. Decline or cancel stays unset and should be prompted again at a later qualifying checkpoint.",
|
|
578
722
|
inputSchema: {
|
|
579
723
|
type: "object",
|
|
580
724
|
properties: {
|
|
@@ -765,11 +909,11 @@ export function listToolSpecs(opts = {}) {
|
|
|
765
909
|
},
|
|
766
910
|
{
|
|
767
911
|
name: canonicalToolNames.checkpointByContext,
|
|
768
|
-
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem
|
|
912
|
+
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem returns a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. ${memoryCitationInstruction} Current time: ${currentTime}.`),
|
|
769
913
|
inputSchema: {
|
|
770
914
|
type: "object",
|
|
771
915
|
properties: {
|
|
772
|
-
contextId: { type: "string", description: "The EchoMem contextId
|
|
916
|
+
contextId: { type: "string", description: "The EchoMem contextId returned by save_conversation or Renew session." },
|
|
773
917
|
limit: { type: "number", default: 100 },
|
|
774
918
|
triggerMessage: {
|
|
775
919
|
type: "string",
|
|
@@ -787,7 +931,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
787
931
|
},
|
|
788
932
|
{
|
|
789
933
|
name: canonicalToolNames.updateStatus,
|
|
790
|
-
description: `Check whether this installed EchoMem MCP bridge is behind the latest
|
|
934
|
+
description: `Check whether this installed EchoMem MCP bridge is behind the latest version. Works without login, uploads no user transcript, and normal background checks are cached. ${MCP_DESKTOP_MANAGED ? "Echo Desktop owns updates for this runtime; direct the user back to the desktop app." : "If it reports an update, tell the user and offer to run the returned update command."} After updating, the user must start a new agent/MCP session.${updateSection}`,
|
|
791
935
|
inputSchema: {
|
|
792
936
|
type: "object",
|
|
793
937
|
properties: {
|
|
@@ -801,7 +945,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
801
945
|
},
|
|
802
946
|
{
|
|
803
947
|
name: canonicalToolNames.contextHealth,
|
|
804
|
-
description: "Show the current local Codex/Claude context-health score as markdown: clean percentage, tracked lower-bound dead-weight, redundant reads, source client, and token-count source. Use when the user asks about
|
|
948
|
+
description: "Show the current local Codex/Claude context-health score as markdown: clean percentage, tracked lower-bound dead-weight, redundant reads, source client, and token-count source. Use when the user asks about dirty context, context pollution, or whether cleanup is worth it. This on-demand tool reads local agent logs only and needs no login.",
|
|
805
949
|
inputSchema: {
|
|
806
950
|
type: "object",
|
|
807
951
|
properties: {
|
|
@@ -841,4 +985,5 @@ export function listToolSpecs(opts = {}) {
|
|
|
841
985
|
},
|
|
842
986
|
},
|
|
843
987
|
];
|
|
988
|
+
return tools.map(decorateLocalToolSpec);
|
|
844
989
|
}
|