@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/crypto.js +4 -0
- package/dist/encryption.js +14 -2
- package/dist/index.js +52 -117
- package/dist/migrate.js +38 -1
- package/dist/setup-page/client-core.js +194 -27
- package/dist/setup-page/client-extraction.js +73 -35
- package/dist/setup-page/client-lifecycle.js +1 -5
- package/dist/setup-page/styles-extraction.js +125 -0
- package/dist/setup-page.js +1 -0
- package/dist/setup-preview.js +127 -38
- package/dist/setup.js +275 -31
- package/dist/v1-contract.js +1 -11
- package/package.json +6 -2
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
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
37
|
-
//
|
|
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
|
|
1195
|
-
* serves local Wrapped stats, and holds the /migrate
|
|
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(`
|
|
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()}]
|
|
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, {
|
|
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.
|
|
2043
|
-
//
|
|
2044
|
-
console.log("Opening your browser to
|
|
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
|
|
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
|
|
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
|
package/dist/v1-contract.js
CHANGED
|
@@ -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
|
|
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.
|
|
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/
|
|
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": {
|