@echomem/mcp 1.4.20 → 1.4.21

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/setup.js CHANGED
@@ -2,15 +2,15 @@
2
2
  * `echomem-mcp init | setup | login | unlock | status | logout` — onboarding for the local bridge (spec §8).
3
3
  *
4
4
  * Design goals from the spec:
5
- * - One command → one browser approval → one reload.
5
+ * - One command → one local browser setup → one reload.
6
6
  * - Both secrets (API token + encryption key) ride a single browser flow and land in the local
7
7
  * keystore — never in the client's MCP config, never in the agent's chat context.
8
8
  * - Re-unlock after the key's TTL is one step, not a re-setup.
9
9
  *
10
- * The browser flow posts `{ token, key? }` to a localhost callback this process opens. The
11
- * "connect device" web page that drives it is the one piece that lives in the web app (not here);
12
- * until it ships, the same flow is fully usable via the manual flags (`--token`, `--key`,
13
- * `--passphrase`), which is also the documented headless/SSH path (spec §8).
10
+ * The browser flow stays on a localhost setup page. That local page collects email OTP consent and
11
+ * an encryption passphrase, while this process asks the hosted API to send/verify OTP and mint a
12
+ * device token. Secrets land in the local keystore never in the client's MCP config, never in
13
+ * the agent's chat context.
14
14
  */
15
15
  import http from "node:http";
16
16
  import { randomUUID } from "node:crypto";
@@ -23,7 +23,7 @@ import readline from "node:readline";
23
23
  import { fileURLToPath, pathToFileURL } from "node:url";
24
24
  import axios from "axios";
25
25
  import { KeyStore } from "./keystore.js";
26
- import { fetchEncryptionConfig, deriveAndVerifyKey, verifyKeyB64 } from "./encryption.js";
26
+ import { fetchEncryptionConfig, deriveAndVerifyKey, setupNewEncryptionKey, verifyKeyB64 } from "./encryption.js";
27
27
  import { collect, runReport, buildStatsPayload } from "./report.js";
28
28
  import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableFastDiscovery, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
29
29
  import { syncCodexUsage } from "./codex-sync.js";
@@ -33,10 +33,8 @@ import { repoLabel, validateForensicReportForSetup } from "./forensics.js";
33
33
  import { installHooks } from "./hud/hooks.js";
34
34
  import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
35
35
  import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
36
- // The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
37
- // served by this localhost bridge, where local logs and processed/unprocessed counts never leave the
38
- // device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
39
- const WEB_URL = (process.env.ECHO_WEB_URL || "https://yeahecho.com").replace(/\/$/, "");
36
+ // The setup dashboard, account login, and encryption passphrase entry are all served by this
37
+ // localhost bridge. The hosted API only sends OTP email, verifies the code, and mints a device token.
40
38
  const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
41
39
  const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
42
40
  const CODEX_SKILL_NAMES = [
@@ -1191,9 +1189,9 @@ function publicRunningForensicProgress(value) {
1191
1189
  };
1192
1190
  }
1193
1191
  /**
1194
- * Start the persistent localhost bridge used by the connect-device page. It accepts the token,
1195
- * serves local Wrapped stats, and holds the /migrate response until cmdLogin has created a cloud
1196
- * import session.
1192
+ * Start the persistent localhost bridge used by the setup page. It sends/verifies OTP through the
1193
+ * hosted API, accepts the local passphrase, serves local Wrapped stats, and holds the /migrate
1194
+ * response until cmdLogin has created a cloud import session.
1197
1195
  */
1198
1196
  export function startCallbackServer(opts = {}) {
1199
1197
  const timeoutMs = opts.timeoutMs ?? 15 * 60_000;
@@ -1211,6 +1209,7 @@ export function startCallbackServer(opts = {}) {
1211
1209
  let authUrl = "";
1212
1210
  let switchAccountUrl = "";
1213
1211
  let connected = false;
1212
+ let pendingLocalAuth = null;
1214
1213
  let reportConsentGranted = opts.requireReportConsent !== true;
1215
1214
  let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
1216
1215
  let migrateStarted = false;
@@ -1227,6 +1226,26 @@ export function startCallbackServer(opts = {}) {
1227
1226
  const checkNonce = (nonce) => !expectedNonce || nonce === expectedNonce;
1228
1227
  const text = (res, status, body = "") => res.writeHead(status, { "Content-Type": "text/plain" }).end(body);
1229
1228
  const json = (res, status, body) => res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));
1229
+ const revokeDeviceToken = async (token) => {
1230
+ try {
1231
+ await authedAxios(token).post("/api/extension/mcp/local-auth/revoke-device", {}, { timeout: 10_000 });
1232
+ return true;
1233
+ }
1234
+ catch (error) {
1235
+ console.error(`Could not revoke incomplete local login: ${error instanceof Error ? error.message : String(error)}`);
1236
+ return false;
1237
+ }
1238
+ };
1239
+ const revokePendingLocalAuth = async () => {
1240
+ const pending = pendingLocalAuth;
1241
+ pendingLocalAuth = null;
1242
+ if (!pending)
1243
+ return false;
1244
+ return revokeDeviceToken(pending.token);
1245
+ };
1246
+ const completeDeviceLogin = async (token) => {
1247
+ await authedAxios(token).post("/api/extension/mcp/local-auth/complete-device", {}, { timeout: 10_000 });
1248
+ };
1230
1249
  const close = () => {
1231
1250
  if (timer)
1232
1251
  clearTimeout(timer);
@@ -1234,6 +1253,7 @@ export function startCallbackServer(opts = {}) {
1234
1253
  if (closed)
1235
1254
  return;
1236
1255
  closed = true;
1256
+ void revokePendingLocalAuth();
1237
1257
  server.close();
1238
1258
  for (const socket of sockets)
1239
1259
  socket.destroy();
@@ -1245,7 +1265,7 @@ export function startCallbackServer(opts = {}) {
1245
1265
  const waitMs = onToken.settled() ? dashboardTimeoutMs : timeoutMs;
1246
1266
  timer = setTimeout(() => {
1247
1267
  if (!onToken.settled()) {
1248
- onToken.reject(new Error(`browser approval did not finish in ${approvalTimeoutLabel}`));
1268
+ onToken.reject(new Error(`local setup did not finish in ${approvalTimeoutLabel}`));
1249
1269
  }
1250
1270
  else if (!decision.settled()) {
1251
1271
  decision.resolve("timeout");
@@ -1265,7 +1285,7 @@ export function startCallbackServer(opts = {}) {
1265
1285
  }
1266
1286
  if (!token)
1267
1287
  return void text(res, 400, "missing token");
1268
- console.log(`[${new Date().toISOString()}] Browser approval callback received.`);
1288
+ console.log(`[${new Date().toISOString()}] Device token callback received.`);
1269
1289
  const firstToken = !onToken.settled();
1270
1290
  connected = true;
1271
1291
  const setupPath = `/setup?nonce=${encodeURIComponent(nonce || "")}&connected=1`;
@@ -1281,12 +1301,172 @@ export function startCallbackServer(opts = {}) {
1281
1301
  }
1282
1302
  armTimeout();
1283
1303
  };
1304
+ const rejectIfLocalAuthBlocked = (res, nonce) => {
1305
+ if (!checkNonce(nonce)) {
1306
+ text(res, 403, "bad nonce");
1307
+ return true;
1308
+ }
1309
+ if (opts.requireReportConsent === true && !reportConsentGranted) {
1310
+ json(res, 403, {
1311
+ error: "LOCAL_HISTORY_CONSENT_REQUIRED",
1312
+ message: "Allow local history access in the setup page before connecting EchoMem.",
1313
+ });
1314
+ return true;
1315
+ }
1316
+ return false;
1317
+ };
1318
+ const resolveLocalToken = (token, key) => {
1319
+ const firstToken = !onToken.settled();
1320
+ connected = true;
1321
+ pendingLocalAuth = null;
1322
+ const callbackToken = { token, key };
1323
+ if (firstToken) {
1324
+ onToken.resolve(callbackToken);
1325
+ }
1326
+ else if (tokenRefreshHandler) {
1327
+ Promise.resolve(tokenRefreshHandler(callbackToken)).catch((e) => {
1328
+ console.error(`Could not refresh local login: ${e instanceof Error ? e.message : String(e)}`);
1329
+ });
1330
+ }
1331
+ armTimeout();
1332
+ };
1333
+ const handleLocalSendOtp = async (res, body) => {
1334
+ if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
1335
+ return;
1336
+ const email = normalizeEmail(body.email);
1337
+ if (!validEmail(email))
1338
+ return void json(res, 400, { ok: false, error: "Invalid email address" });
1339
+ if (!acceptedLocalTerms(body)) {
1340
+ return void json(res, 400, {
1341
+ ok: false,
1342
+ error: "Confirm your age and accept Echo's terms before continuing.",
1343
+ });
1344
+ }
1345
+ try {
1346
+ const response = await apiAxios().post("/api/extension/mcp/local-auth/send-otp", {
1347
+ email,
1348
+ acceptedTerms: true,
1349
+ ageConfirmed: true,
1350
+ }, { timeout: 10_000 });
1351
+ json(res, 200, { ok: true, email: asString(response.data?.email) || email });
1352
+ }
1353
+ catch (error) {
1354
+ const detail = publicAxiosError(error, "Failed to send verification code");
1355
+ json(res, detail.status, { ok: false, error: detail.message });
1356
+ }
1357
+ };
1358
+ const handleLocalVerifyOtp = async (res, body) => {
1359
+ if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
1360
+ return;
1361
+ const email = normalizeEmail(body.email);
1362
+ const otp = asString(body.otp)?.trim() || "";
1363
+ if (!validEmail(email))
1364
+ return void json(res, 400, { ok: false, error: "Invalid email address" });
1365
+ if (!/^\d{6}$/.test(otp)) {
1366
+ return void json(res, 400, { ok: false, error: "Enter the 6-digit verification code." });
1367
+ }
1368
+ if (!acceptedLocalTerms(body)) {
1369
+ return void json(res, 400, {
1370
+ ok: false,
1371
+ error: "Confirm your age and accept Echo's terms before continuing.",
1372
+ });
1373
+ }
1374
+ try {
1375
+ const response = await apiAxios().post("/api/extension/mcp/local-auth/verify-otp", {
1376
+ email,
1377
+ otp,
1378
+ acceptedTerms: true,
1379
+ ageConfirmed: true,
1380
+ }, { timeout: 15_000 });
1381
+ const token = asString(response.data?.api_key);
1382
+ const userId = asString(response.data?.user_id);
1383
+ if (!token || !userId) {
1384
+ return void json(res, 502, { ok: false, error: "The EchoMem API did not return a device token." });
1385
+ }
1386
+ let config;
1387
+ try {
1388
+ config = await fetchEncryptionConfig(authedAxios(token));
1389
+ }
1390
+ catch (error) {
1391
+ await revokeDeviceToken(token);
1392
+ throw error;
1393
+ }
1394
+ await revokePendingLocalAuth();
1395
+ pendingLocalAuth = {
1396
+ token,
1397
+ userId,
1398
+ email: asString(response.data?.email) || email,
1399
+ mode: config.enabled ? "unlock" : "setup",
1400
+ config: config.enabled ? config : undefined,
1401
+ expiresAtMs: Date.now() + 10 * 60_000,
1402
+ };
1403
+ json(res, 200, {
1404
+ ok: true,
1405
+ stage: "passphrase",
1406
+ mode: pendingLocalAuth.mode,
1407
+ email: pendingLocalAuth.email,
1408
+ });
1409
+ }
1410
+ catch (error) {
1411
+ const detail = publicAxiosError(error, "Failed to verify code");
1412
+ json(res, detail.status, { ok: false, error: detail.message });
1413
+ }
1414
+ };
1415
+ const handleLocalPassphrase = async (res, body) => {
1416
+ if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
1417
+ return;
1418
+ const passphrase = typeof body.passphrase === "string" ? body.passphrase : "";
1419
+ if (passphrase.length < 4) {
1420
+ return void json(res, 400, { ok: false, error: "Enter an encryption passphrase with at least 4 characters." });
1421
+ }
1422
+ if (!pendingLocalAuth || pendingLocalAuth.expiresAtMs <= Date.now()) {
1423
+ await revokePendingLocalAuth();
1424
+ return void json(res, 409, {
1425
+ ok: false,
1426
+ error: "This local login expired. Send a new verification code.",
1427
+ reset: true,
1428
+ });
1429
+ }
1430
+ try {
1431
+ if (pendingLocalAuth.mode === "unlock") {
1432
+ const key = pendingLocalAuth.config
1433
+ ? await deriveAndVerifyKey(passphrase, pendingLocalAuth.config)
1434
+ : null;
1435
+ if (!key)
1436
+ return void json(res, 400, { ok: false, error: "Incorrect encryption passphrase." });
1437
+ await completeDeviceLogin(pendingLocalAuth.token);
1438
+ resolveLocalToken(pendingLocalAuth.token, key);
1439
+ return void json(res, 200, { ok: true, connected: true });
1440
+ }
1441
+ const setup = await setupNewEncryptionKey(passphrase);
1442
+ await authedAxios(pendingLocalAuth.token).post("/api/extension/account/encryption", {
1443
+ salt: setup.saltBase64,
1444
+ verification: setup.verification,
1445
+ iterations: setup.iterations,
1446
+ }, {
1447
+ timeout: 10_000,
1448
+ headers: { "X-Encryption-Key": setup.keyBase64 },
1449
+ });
1450
+ pendingLocalAuth.mode = "unlock";
1451
+ pendingLocalAuth.config = {
1452
+ enabled: true,
1453
+ salt: setup.saltBase64,
1454
+ verification: setup.verification,
1455
+ iterations: setup.iterations,
1456
+ };
1457
+ await completeDeviceLogin(pendingLocalAuth.token);
1458
+ resolveLocalToken(pendingLocalAuth.token, setup.keyBase64);
1459
+ json(res, 200, { ok: true, connected: true });
1460
+ }
1461
+ catch (error) {
1462
+ const detail = publicAxiosError(error, "Failed to unlock encrypted memory");
1463
+ json(res, detail.status, { ok: false, error: detail.message });
1464
+ }
1465
+ };
1284
1466
  server = http.createServer((req, res) => {
1285
1467
  res.setHeader("Access-Control-Allow-Origin", "*");
1286
1468
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1287
1469
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1288
- // Let the HTTPS connect-device page reach this localhost server (Chrome Private Network Access).
1289
- res.setHeader("Access-Control-Allow-Private-Network", "true");
1290
1470
  if (req.method === "OPTIONS")
1291
1471
  return void res.writeHead(204).end();
1292
1472
  const url = new URL(req.url || "/", "http://127.0.0.1");
@@ -1330,6 +1510,7 @@ export function startCallbackServer(opts = {}) {
1330
1510
  authUrl,
1331
1511
  switchAccountUrl: switchAccountUrl || authUrl,
1332
1512
  localOnly: true,
1513
+ localAuth: true,
1333
1514
  workspacePath: process.cwd(),
1334
1515
  consentRequired: opts.requireReportConsent === true,
1335
1516
  consentGranted: reportConsentGranted,
@@ -1385,6 +1566,42 @@ export function startCallbackServer(opts = {}) {
1385
1566
  handleCallback(res, asString(body.token), asString(body.key), asString(body.nonce));
1386
1567
  return;
1387
1568
  }
1569
+ if (route === "/local-auth/send-otp" && req.method === "POST") {
1570
+ let body;
1571
+ try {
1572
+ body = await readJsonBody(req);
1573
+ }
1574
+ catch {
1575
+ text(res, 400, "bad json");
1576
+ return;
1577
+ }
1578
+ await handleLocalSendOtp(res, body);
1579
+ return;
1580
+ }
1581
+ if (route === "/local-auth/verify-otp" && req.method === "POST") {
1582
+ let body;
1583
+ try {
1584
+ body = await readJsonBody(req);
1585
+ }
1586
+ catch {
1587
+ text(res, 400, "bad json");
1588
+ return;
1589
+ }
1590
+ await handleLocalVerifyOtp(res, body);
1591
+ return;
1592
+ }
1593
+ if (route === "/local-auth/passphrase" && req.method === "POST") {
1594
+ let body;
1595
+ try {
1596
+ body = await readJsonBody(req);
1597
+ }
1598
+ catch {
1599
+ text(res, 400, "bad json");
1600
+ return;
1601
+ }
1602
+ await handleLocalPassphrase(res, body);
1603
+ return;
1604
+ }
1388
1605
  if (route === "/stats" && req.method === "GET") {
1389
1606
  if (!checkNonce(url.searchParams.get("nonce") || undefined))
1390
1607
  return void text(res, 403, "bad nonce");
@@ -1599,10 +1816,17 @@ export function startCallbackServer(opts = {}) {
1599
1816
  /* already logged out locally */
1600
1817
  }
1601
1818
  connected = false;
1819
+ const revokedPendingCredential = await revokePendingLocalAuth();
1602
1820
  stats = null;
1603
1821
  migrateStarted = false;
1604
1822
  progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
1605
- json(res, 200, { ok: true, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true });
1823
+ json(res, 200, {
1824
+ ok: true,
1825
+ authUrl,
1826
+ switchAccountUrl: switchAccountUrl || authUrl,
1827
+ localOnly: true,
1828
+ revokedPendingCredential,
1829
+ });
1606
1830
  Promise.resolve()
1607
1831
  .then(() => logoutHandler?.())
1608
1832
  .catch((e) => {
@@ -1726,6 +1950,34 @@ function authedAxios(token) {
1726
1950
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
1727
1951
  });
1728
1952
  }
1953
+ function apiAxios() {
1954
+ return axios.create({
1955
+ baseURL: API_BASE_URL,
1956
+ headers: { "Content-Type": "application/json" },
1957
+ });
1958
+ }
1959
+ function normalizeEmail(value) {
1960
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
1961
+ }
1962
+ function validEmail(value) {
1963
+ return value.length > 0 && value.length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
1964
+ }
1965
+ function acceptedLocalTerms(body) {
1966
+ return body.acceptedTerms === true && body.ageConfirmed === true;
1967
+ }
1968
+ function publicAxiosError(error, fallback) {
1969
+ if (axios.isAxiosError(error)) {
1970
+ const data = error.response?.data;
1971
+ const status = typeof error.response?.status === "number" ? error.response.status : 500;
1972
+ const message = typeof data?.error === "string"
1973
+ ? data.error
1974
+ : typeof data?.message === "string"
1975
+ ? data.message
1976
+ : fallback;
1977
+ return { message, status };
1978
+ }
1979
+ return { message: error instanceof Error ? error.message : fallback, status: 500 };
1980
+ }
1729
1981
  function formatVerificationError(error) {
1730
1982
  if (axios.isAxiosError(error)) {
1731
1983
  const status = typeof error.response?.status === "number" ? error.response.status : null;
@@ -2039,9 +2291,9 @@ async function cmdLogin(flags) {
2039
2291
  process.exitCode = 1;
2040
2292
  return ok;
2041
2293
  }
2042
- // Browser path: open a localhost dashboard. It briefly leaves for hosted auth, then returns here
2043
- // after the web page has delivered the token+key to the callback. The nonce gates every local route.
2044
- console.log("Opening your browser to approve this device…");
2294
+ // Browser path: open a localhost dashboard. Login, legal confirmation, and encryption
2295
+ // passphrase entry all happen through this local bridge. The nonce gates every local route.
2296
+ console.log("Opening your browser to connect this device locally…");
2045
2297
  const devPortRaw = typeof flags["dev-port"] === "string" ? Number(flags["dev-port"]) : undefined;
2046
2298
  if (devPortRaw !== undefined && (!Number.isInteger(devPortRaw) || devPortRaw < 1024 || devPortRaw > 65535)) {
2047
2299
  throw new Error("--dev-port must be an integer between 1024 and 65535");
@@ -2113,18 +2365,10 @@ async function cmdLogin(flags) {
2113
2365
  startForensicScan();
2114
2366
  },
2115
2367
  });
2116
- const callbackUrl = `http://127.0.0.1:${srv.port}/callback`;
2117
2368
  const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
2118
- const connectUrl = `${WEB_URL}/connect-device?callback=${encodeURIComponent(callbackUrl)}&nonce=${nonce}&return_to=${encodeURIComponent(localSetupUrl)}`;
2119
- const switchAccountUrl = new URL(connectUrl);
2120
- // The hosted connect-device page should clear its own Supabase/browser session before minting
2121
- // the localhost token when this hint is present. Localhost cannot safely clear yeahecho.com auth.
2122
- switchAccountUrl.searchParams.set("force_signout", "1");
2123
- switchAccountUrl.searchParams.set("prompt", "login");
2124
- srv.setAuthUrl(connectUrl, switchAccountUrl.toString());
2125
2369
  openBrowser(localSetupUrl);
2126
2370
  console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
2127
- console.log("Waiting for browser approval for up to 15 minutes…");
2371
+ console.log("Waiting for local setup for up to 15 minutes…");
2128
2372
  const startForensicScan = () => {
2129
2373
  if (forensicScanStarted || forensicConsent !== "allowed")
2130
2374
  return;
@@ -2880,7 +3124,7 @@ Usage:
2880
3124
  echomem-mcp update --all Repoint detected clients to this installed bridge; no login/browser
2881
3125
  echomem-mcp update --client X Repoint one MCP client; no login/browser
2882
3126
  echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
2883
- echomem-mcp login Approve this device in the browser (or --token/--passphrase)
3127
+ echomem-mcp login Connect this device in the local browser page (or --token/--passphrase)
2884
3128
  echomem-mcp unlock Privately unlock the vault on this trusted device
2885
3129
  echomem-mcp lock Remove the local vault key while keeping the device login
2886
3130
  echomem-mcp status Show token/key/clients
@@ -117,7 +117,7 @@ export function listToolSpecs(opts = {}) {
117
117
  return [
118
118
  {
119
119
  name: canonicalToolNames.search,
120
- 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}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
120
+ 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}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
121
121
  inputSchema: {
122
122
  type: "object",
123
123
  properties: {
@@ -125,11 +125,6 @@ export function listToolSpecs(opts = {}) {
125
125
  limit: { type: "number", default: 10 },
126
126
  threshold: { type: "number", default: 0.1 },
127
127
  timeFrameDays: { type: "number" },
128
- includeAnswer: {
129
- type: "boolean",
130
- default: false,
131
- description: "Default false: return only retrieved memories and skip answer generation. Set true for the legacy synthesized recall answer.",
132
- },
133
128
  triggerMessage: {
134
129
  type: "string",
135
130
  description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
@@ -148,11 +143,6 @@ export function listToolSpecs(opts = {}) {
148
143
  limit: { type: "number", default: 10 },
149
144
  threshold: { type: "number", default: 0.1 },
150
145
  timeFrameDays: { type: "number" },
151
- includeAnswer: {
152
- type: "boolean",
153
- default: false,
154
- description: "Default false: return only retrieved memories and skip answer generation. Set true for the legacy synthesized recall answer.",
155
- },
156
146
  triggerMessage: {
157
147
  type: "string",
158
148
  description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.20",
3
+ "version": "1.4.21",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,7 +22,11 @@
22
22
  "dev": "node dist/index.js",
23
23
  "smoke": "node smoke.mjs",
24
24
  "preview:extraction": "npm run build && node scripts/preview-extraction.mjs",
25
- "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/hud.test.mjs",
25
+ "test:artifact": "npm run build && node test/package-artifact.test.mjs",
26
+ "test:registry": "node test/registry-artifact.test.mjs",
27
+ "test:registry-ui": "npm run build && node test/registry-ui.test.mjs",
28
+ "test:ui": "npm run build && node test/setup-ui.test.mjs",
29
+ "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs",
26
30
  "prepack": "npm run build && node scripts/bundle-city.mjs"
27
31
  },
28
32
  "dependencies": {