@echomem/mcp 1.4.21 → 1.4.22
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 +1 -1
- package/assets/hud/echo-pricing-free-sticker.png +0 -0
- package/assets/hud/echo-pricing-power-sticker.png +0 -0
- package/assets/hud/echo-pricing-pro-sticker.png +0 -0
- package/assets/hud/scan-loop/frame-01-ready.png +0 -0
- package/assets/hud/scan-loop/frame-02-open.png +0 -0
- package/assets/hud/scan-loop/frame-03-pull.png +0 -0
- package/assets/hud/scan-loop/frame-04-read.png +0 -0
- package/assets/hud/scan-loop/frame-05-file.png +0 -0
- package/assets/hud/scan-loop/frame-06-close.png +0 -0
- package/dist/setup-page/client-core.js +140 -47
- package/dist/setup-page/client-extraction.js +251 -68
- package/dist/setup-page/client-lifecycle.js +45 -39
- package/dist/setup-page/client-report-audit.js +1 -1
- package/dist/setup-page/client-report-city.js +35 -22
- package/dist/setup-page/document.js +1 -1
- package/dist/setup-page/styles-extraction.js +330 -0
- package/dist/setup-page/styles-mvp.js +976 -0
- package/dist/setup-page/styles-website-alignment.js +237 -5
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +18 -2
- package/dist/setup.js +262 -40
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `echomem-mcp init | setup | login | unlock | status | logout` —
|
|
2
|
+
* `echomem-mcp init | setup | login | unlock | status | logout` — local bridge lifecycle (spec §8).
|
|
3
3
|
*
|
|
4
4
|
* Design goals from the spec:
|
|
5
|
-
* -
|
|
5
|
+
* - `login` establishes the account and trusted device only; it never reads local history.
|
|
6
|
+
* - `init` runs one ordered flow: local-history permission/report, login, plan if needed, then extraction.
|
|
6
7
|
* - Both secrets (API token + encryption key) ride a single browser flow and land in the local
|
|
7
8
|
* keystore — never in the client's MCP config, never in the agent's chat context.
|
|
8
9
|
* - Re-unlock after the key's TTL is one step, not a re-setup.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* device token. Secrets land in the local keystore —
|
|
13
|
-
* the agent's chat context.
|
|
11
|
+
* Both browser flows stay on a localhost page. Login collects email OTP consent and an encryption
|
|
12
|
+
* passphrase; onboarding separately asks to access local coding history. This process asks the
|
|
13
|
+
* hosted API to send/verify OTP and mint a device token. Secrets land in the local keystore —
|
|
14
|
+
* never in the client's MCP config or the agent's chat context.
|
|
14
15
|
*/
|
|
15
16
|
import http from "node:http";
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
@@ -37,6 +38,28 @@ import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "
|
|
|
37
38
|
// localhost bridge. The hosted API only sends OTP email, verifies the code, and mints a device token.
|
|
38
39
|
const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
39
40
|
const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
|
|
41
|
+
function hostedBillingEndpoint(pathname) {
|
|
42
|
+
const url = new URL(PRICING_URL);
|
|
43
|
+
url.pathname = pathname;
|
|
44
|
+
url.search = "";
|
|
45
|
+
url.hash = "";
|
|
46
|
+
return url.toString();
|
|
47
|
+
}
|
|
48
|
+
function isExpectedStripeUrl(value, kind) {
|
|
49
|
+
if (typeof value !== "string")
|
|
50
|
+
return false;
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(value);
|
|
53
|
+
if (url.protocol !== "https:")
|
|
54
|
+
return false;
|
|
55
|
+
return kind === "checkout"
|
|
56
|
+
? url.hostname === "checkout.stripe.com"
|
|
57
|
+
: url.hostname === "billing.stripe.com";
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
40
63
|
const CODEX_SKILL_NAMES = [
|
|
41
64
|
"echomem-search",
|
|
42
65
|
"echomem-save",
|
|
@@ -266,6 +289,52 @@ function resolveGlobalEntry() {
|
|
|
266
289
|
}
|
|
267
290
|
return null;
|
|
268
291
|
}
|
|
292
|
+
export function needsDurableGlobalUpdate(runningEntry, durableEntry, targetVersion = MCP_PACKAGE_VERSION) {
|
|
293
|
+
if (!isEphemeralNpxPath(runningEntry))
|
|
294
|
+
return false;
|
|
295
|
+
const installedVersion = durableEntry
|
|
296
|
+
? packageVersionFromPath(durableEntry)?.version
|
|
297
|
+
: undefined;
|
|
298
|
+
return installedVersion !== targetVersion;
|
|
299
|
+
}
|
|
300
|
+
function installDurableGlobalUpdate() {
|
|
301
|
+
const runningEntry = (() => {
|
|
302
|
+
try {
|
|
303
|
+
return fs.realpathSync(process.argv[1] || "");
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return process.argv[1] || "";
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
const currentGlobalEntry = resolveGlobalEntry();
|
|
310
|
+
if (!needsDurableGlobalUpdate(runningEntry, currentGlobalEntry))
|
|
311
|
+
return;
|
|
312
|
+
const packageSpec = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
|
|
313
|
+
console.log(`Installing durable ${packageSpec} before updating client configs…`);
|
|
314
|
+
const npmExecPath = process.env.npm_execpath;
|
|
315
|
+
try {
|
|
316
|
+
if (npmExecPath && fs.existsSync(npmExecPath)) {
|
|
317
|
+
execFileSync(process.execPath, [npmExecPath, "install", "-g", packageSpec], {
|
|
318
|
+
stdio: "inherit",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "-g", packageSpec], {
|
|
323
|
+
stdio: "inherit",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
throw new Error(`Could not install ${packageSpec} globally: ${error instanceof Error ? error.message : String(error)}`);
|
|
329
|
+
}
|
|
330
|
+
const installedEntry = resolveGlobalEntry();
|
|
331
|
+
const installedVersion = installedEntry
|
|
332
|
+
? packageVersionFromPath(installedEntry)?.version
|
|
333
|
+
: undefined;
|
|
334
|
+
if (installedVersion !== MCP_PACKAGE_VERSION) {
|
|
335
|
+
throw new Error(`Global EchoMem bridge is ${installedVersion || "missing"} after update; expected ${MCP_PACKAGE_VERSION}.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
269
338
|
/** The TOML block EchoMem adds to ~/.codex/config.toml. No secret — the bridge reads the keystore. */
|
|
270
339
|
export function codexTomlBlock(entry) {
|
|
271
340
|
const command = JSON.stringify(String(entry.command));
|
|
@@ -1201,6 +1270,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1201
1270
|
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
1202
1271
|
const expectedNonce = opts.nonce;
|
|
1203
1272
|
const scanId = opts.scanId ?? randomUUID();
|
|
1273
|
+
const flow = opts.flow ?? "onboarding";
|
|
1274
|
+
const isLoginFlow = flow === "login";
|
|
1275
|
+
// A login screen must not be blocked by a local-history permission. That permission belongs to
|
|
1276
|
+
// onboarding and is intentionally enforced separately below.
|
|
1277
|
+
const requiresReportConsent = opts.requireReportConsent === true && !isLoginFlow;
|
|
1204
1278
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
1205
1279
|
const onToken = deferred();
|
|
1206
1280
|
const decision = deferred();
|
|
@@ -1208,9 +1282,10 @@ export function startCallbackServer(opts = {}) {
|
|
|
1208
1282
|
let stats = null;
|
|
1209
1283
|
let authUrl = "";
|
|
1210
1284
|
let switchAccountUrl = "";
|
|
1211
|
-
let connected =
|
|
1285
|
+
let connected = Boolean(opts.initialToken?.token);
|
|
1286
|
+
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1212
1287
|
let pendingLocalAuth = null;
|
|
1213
|
-
let reportConsentGranted =
|
|
1288
|
+
let reportConsentGranted = !requiresReportConsent;
|
|
1214
1289
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
1215
1290
|
let migrateStarted = false;
|
|
1216
1291
|
let tokenRefreshHandler = null;
|
|
@@ -1277,7 +1352,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1277
1352
|
const handleCallback = (res, token, key, nonce) => {
|
|
1278
1353
|
if (!checkNonce(nonce))
|
|
1279
1354
|
return void text(res, 403, "bad nonce");
|
|
1280
|
-
if (
|
|
1355
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1281
1356
|
return void json(res, 403, {
|
|
1282
1357
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1283
1358
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1288,6 +1363,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1288
1363
|
console.log(`[${new Date().toISOString()}] Device token callback received.`);
|
|
1289
1364
|
const firstToken = !onToken.settled();
|
|
1290
1365
|
connected = true;
|
|
1366
|
+
activeDeviceToken = token;
|
|
1291
1367
|
const setupPath = `/setup?nonce=${encodeURIComponent(nonce || "")}&connected=1`;
|
|
1292
1368
|
res.writeHead(200, { "Content-Type": "text/html" }).end(`<!doctype html><html><head><meta charset="utf-8"></head><body style="font-family:system-ui;padding:3rem;text-align:center"><h2>EchoMem connected</h2><p>Returning to the local setup page...</p><p><a href="${setupPath}">Continue</a></p><script>(function(){var target=${JSON.stringify(setupPath)};try{if(window.opener&&!window.opener.closed){window.opener.postMessage({type:"echomem:connected",nonce:${JSON.stringify(nonce || "")}},window.location.origin);window.close();setTimeout(function(){window.location.href=target;},500);return;}}catch(_){}window.location.href=target;})();</script></body></html>`);
|
|
1293
1369
|
const callbackToken = { token, key };
|
|
@@ -1306,7 +1382,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1306
1382
|
text(res, 403, "bad nonce");
|
|
1307
1383
|
return true;
|
|
1308
1384
|
}
|
|
1309
|
-
if (
|
|
1385
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1310
1386
|
json(res, 403, {
|
|
1311
1387
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1312
1388
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1318,6 +1394,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1318
1394
|
const resolveLocalToken = (token, key) => {
|
|
1319
1395
|
const firstToken = !onToken.settled();
|
|
1320
1396
|
connected = true;
|
|
1397
|
+
activeDeviceToken = token;
|
|
1321
1398
|
pendingLocalAuth = null;
|
|
1322
1399
|
const callbackToken = { token, key };
|
|
1323
1400
|
if (firstToken) {
|
|
@@ -1330,6 +1407,17 @@ export function startCallbackServer(opts = {}) {
|
|
|
1330
1407
|
}
|
|
1331
1408
|
armTimeout();
|
|
1332
1409
|
};
|
|
1410
|
+
const isOnboardingOnlyRoute = (route) => [
|
|
1411
|
+
"/report-consent",
|
|
1412
|
+
"/report",
|
|
1413
|
+
"/stats",
|
|
1414
|
+
"/billing-status",
|
|
1415
|
+
"/billing-checkout",
|
|
1416
|
+
"/billing-portal",
|
|
1417
|
+
"/progress",
|
|
1418
|
+
"/migrate",
|
|
1419
|
+
"/skip",
|
|
1420
|
+
].includes(route);
|
|
1333
1421
|
const handleLocalSendOtp = async (res, body) => {
|
|
1334
1422
|
if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
|
|
1335
1423
|
return;
|
|
@@ -1472,6 +1560,15 @@ export function startCallbackServer(opts = {}) {
|
|
|
1472
1560
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
1473
1561
|
const route = url.pathname;
|
|
1474
1562
|
const run = async () => {
|
|
1563
|
+
// Defense in depth: the login bridge never exposes the routes that can inspect or move
|
|
1564
|
+
// local conversation history. The browser UI also routes around them, but the server is
|
|
1565
|
+
// the authority for this privacy boundary.
|
|
1566
|
+
if (isLoginFlow && isOnboardingOnlyRoute(route)) {
|
|
1567
|
+
return void json(res, 404, {
|
|
1568
|
+
error: "ONBOARDING_REQUIRED",
|
|
1569
|
+
message: "Run `echomem-mcp init` to access local-history onboarding.",
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1475
1572
|
if ((route === "/city" || route.startsWith("/city/")) && req.method === "GET") {
|
|
1476
1573
|
serveRepoCityAsset(route, res);
|
|
1477
1574
|
return;
|
|
@@ -1507,12 +1604,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
1507
1604
|
return void text(res, 403, "bad nonce");
|
|
1508
1605
|
json(res, 200, {
|
|
1509
1606
|
connected,
|
|
1607
|
+
flow,
|
|
1510
1608
|
authUrl,
|
|
1511
1609
|
switchAccountUrl: switchAccountUrl || authUrl,
|
|
1512
1610
|
localOnly: true,
|
|
1513
1611
|
localAuth: true,
|
|
1514
1612
|
workspacePath: process.cwd(),
|
|
1515
|
-
consentRequired:
|
|
1613
|
+
consentRequired: requiresReportConsent,
|
|
1516
1614
|
consentGranted: reportConsentGranted,
|
|
1517
1615
|
});
|
|
1518
1616
|
return;
|
|
@@ -1605,7 +1703,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1605
1703
|
if (route === "/stats" && req.method === "GET") {
|
|
1606
1704
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1607
1705
|
return void text(res, 403, "bad nonce");
|
|
1608
|
-
if (
|
|
1706
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1609
1707
|
return void json(res, 403, {
|
|
1610
1708
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1611
1709
|
message: "Allow local history access before continuing setup.",
|
|
@@ -1620,13 +1718,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
1620
1718
|
if (route === "/billing-status" && req.method === "GET") {
|
|
1621
1719
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1622
1720
|
return void text(res, 403, "bad nonce");
|
|
1623
|
-
if (
|
|
1721
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1624
1722
|
return void json(res, 403, {
|
|
1625
1723
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1626
1724
|
message: "Allow local history access before continuing setup.",
|
|
1627
1725
|
});
|
|
1628
1726
|
}
|
|
1629
|
-
const token = new KeyStore().getToken();
|
|
1727
|
+
const token = activeDeviceToken || new KeyStore().getToken();
|
|
1630
1728
|
const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
|
|
1631
1729
|
if (!token) {
|
|
1632
1730
|
json(res, 200, { plan: "free", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
|
|
@@ -1662,12 +1760,84 @@ export function startCallbackServer(opts = {}) {
|
|
|
1662
1760
|
}
|
|
1663
1761
|
return;
|
|
1664
1762
|
}
|
|
1763
|
+
if ((route === "/billing-checkout" || route === "/billing-portal") && req.method === "POST") {
|
|
1764
|
+
let body;
|
|
1765
|
+
try {
|
|
1766
|
+
body = await readJsonBody(req);
|
|
1767
|
+
}
|
|
1768
|
+
catch {
|
|
1769
|
+
text(res, 400, "bad json");
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (!checkNonce(asString(body.nonce)))
|
|
1773
|
+
return void text(res, 403, "bad nonce");
|
|
1774
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1775
|
+
return void json(res, 403, {
|
|
1776
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1777
|
+
message: "Allow local history access before managing an onboarding plan.",
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
const token = activeDeviceToken || new KeyStore().getToken();
|
|
1781
|
+
if (!connected || !token) {
|
|
1782
|
+
return void json(res, 401, {
|
|
1783
|
+
error: "ECHOMEM_LOGIN_REQUIRED",
|
|
1784
|
+
message: "Connect your EchoMem account before continuing to billing.",
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
const isCheckout = route === "/billing-checkout";
|
|
1788
|
+
const requestBody = { source: "mcp_onboarding" };
|
|
1789
|
+
if (isCheckout) {
|
|
1790
|
+
const plan = asString(body.plan)?.toLowerCase();
|
|
1791
|
+
if (plan !== "pro" && plan !== "power") {
|
|
1792
|
+
return void json(res, 400, {
|
|
1793
|
+
error: "INVALID_PLAN",
|
|
1794
|
+
message: "Choose Pro or Power to continue to checkout.",
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
requestBody.plan = plan;
|
|
1798
|
+
requestBody.trial = body.trial === true;
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
const plan = asString(body.plan)?.toLowerCase();
|
|
1802
|
+
if (plan) {
|
|
1803
|
+
if (plan !== "pro" && plan !== "power") {
|
|
1804
|
+
return void json(res, 400, {
|
|
1805
|
+
error: "INVALID_PLAN",
|
|
1806
|
+
message: "Choose Pro or Power to change plans.",
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
requestBody.plan = plan;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const response = await axios.post(hostedBillingEndpoint(isCheckout ? "/api/billing/checkout" : "/api/billing/portal"), requestBody, {
|
|
1814
|
+
timeout: 15_000,
|
|
1815
|
+
headers: {
|
|
1816
|
+
"Content-Type": "application/json",
|
|
1817
|
+
Authorization: `Bearer ${token}`,
|
|
1818
|
+
},
|
|
1819
|
+
});
|
|
1820
|
+
const hostedUrl = response.data?.url;
|
|
1821
|
+
if (!isExpectedStripeUrl(hostedUrl, isCheckout ? "checkout" : "portal")) {
|
|
1822
|
+
return void json(res, 502, {
|
|
1823
|
+
error: "INVALID_BILLING_DESTINATION",
|
|
1824
|
+
message: "Echo billing returned an unexpected destination.",
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
json(res, 200, { url: hostedUrl });
|
|
1828
|
+
}
|
|
1829
|
+
catch (error) {
|
|
1830
|
+
const detail = publicAxiosError(error, isCheckout ? "Could not start secure checkout." : "Could not open secure plan management.");
|
|
1831
|
+
json(res, detail.status, { error: "BILLING_REQUEST_FAILED", message: detail.message });
|
|
1832
|
+
}
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1665
1835
|
if (route === "/report" && req.method === "GET") {
|
|
1666
1836
|
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
1667
1837
|
res.setHeader("Cache-Control", "no-store");
|
|
1668
1838
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1669
1839
|
return void text(res, 403, "bad nonce");
|
|
1670
|
-
if (
|
|
1840
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1671
1841
|
return void json(res, 403, {
|
|
1672
1842
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1673
1843
|
message: "Allow local history access before starting the local scan.",
|
|
@@ -1768,7 +1938,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1768
1938
|
if (route === "/progress" && req.method === "GET") {
|
|
1769
1939
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1770
1940
|
return void text(res, 403, "bad nonce");
|
|
1771
|
-
if (
|
|
1941
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1772
1942
|
return void json(res, 403, {
|
|
1773
1943
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1774
1944
|
message: "Allow local history access before continuing setup.",
|
|
@@ -1816,6 +1986,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1816
1986
|
/* already logged out locally */
|
|
1817
1987
|
}
|
|
1818
1988
|
connected = false;
|
|
1989
|
+
activeDeviceToken = "";
|
|
1819
1990
|
const revokedPendingCredential = await revokePendingLocalAuth();
|
|
1820
1991
|
stats = null;
|
|
1821
1992
|
migrateStarted = false;
|
|
@@ -1845,7 +2016,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1845
2016
|
}
|
|
1846
2017
|
if (!checkNonce(asString(body.nonce)))
|
|
1847
2018
|
return void text(res, 403, "bad nonce");
|
|
1848
|
-
if (
|
|
2019
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1849
2020
|
return void json(res, 403, {
|
|
1850
2021
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1851
2022
|
message: "Allow local history access before starting extraction.",
|
|
@@ -1906,6 +2077,8 @@ export function startCallbackServer(opts = {}) {
|
|
|
1906
2077
|
sockets.add(socket);
|
|
1907
2078
|
socket.on("close", () => sockets.delete(socket));
|
|
1908
2079
|
});
|
|
2080
|
+
if (opts.initialToken)
|
|
2081
|
+
onToken.resolve(opts.initialToken);
|
|
1909
2082
|
armTimeout();
|
|
1910
2083
|
server.on("error", (e) => rejectOuter(e));
|
|
1911
2084
|
server.listen(opts.port ?? 0, "127.0.0.1", () => {
|
|
@@ -2140,9 +2313,9 @@ async function cmdSetup(flags) {
|
|
|
2140
2313
|
/**
|
|
2141
2314
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
2142
2315
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
2143
|
-
* Codex skills, writes the AGENTS.md memory guidance,
|
|
2144
|
-
*
|
|
2145
|
-
* primitives; init
|
|
2316
|
+
* Codex skills, writes the AGENTS.md memory guidance, and launches the context HUD. One browser
|
|
2317
|
+
* bridge then runs permission → report → login → plan if needed → extraction in that order.
|
|
2318
|
+
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
2146
2319
|
*/
|
|
2147
2320
|
async function cmdInit(flags) {
|
|
2148
2321
|
console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
|
|
@@ -2151,10 +2324,10 @@ async function cmdInit(flags) {
|
|
|
2151
2324
|
// 2. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
|
|
2152
2325
|
if (!flags["no-hud"])
|
|
2153
2326
|
await cmdSetupHud(flags);
|
|
2154
|
-
// 3. Start
|
|
2327
|
+
// 3. Start one ordered onboarding bridge. A fresh device logs in only after consent + report.
|
|
2155
2328
|
console.log("");
|
|
2156
|
-
if (!flags["skip-login"] && !flags["no-login"] && !await
|
|
2157
|
-
console.log("\nEchoMem is configured, but
|
|
2329
|
+
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
2330
|
+
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
2158
2331
|
return;
|
|
2159
2332
|
}
|
|
2160
2333
|
console.log("");
|
|
@@ -2215,6 +2388,8 @@ function writeCodexSkillsForTargets(targets) {
|
|
|
2215
2388
|
}
|
|
2216
2389
|
}
|
|
2217
2390
|
async function cmdUpdate(flags) {
|
|
2391
|
+
if (!flags.dev)
|
|
2392
|
+
installDurableGlobalUpdate();
|
|
2218
2393
|
await cmdSetup({ ...flags, "skip-login": true });
|
|
2219
2394
|
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
2220
2395
|
}
|
|
@@ -2279,6 +2454,19 @@ function parseHudClient(value) {
|
|
|
2279
2454
|
? value
|
|
2280
2455
|
: "auto";
|
|
2281
2456
|
}
|
|
2457
|
+
function localBridgeOptions(flags) {
|
|
2458
|
+
const port = typeof flags["dev-port"] === "string" ? Number(flags["dev-port"]) : undefined;
|
|
2459
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1024 || port > 65535)) {
|
|
2460
|
+
throw new Error("--dev-port must be an integer between 1024 and 65535");
|
|
2461
|
+
}
|
|
2462
|
+
const requestedNonce = typeof flags["dev-nonce"] === "string" ? flags["dev-nonce"].trim() : undefined;
|
|
2463
|
+
if (requestedNonce && port === undefined)
|
|
2464
|
+
throw new Error("--dev-nonce requires --dev-port");
|
|
2465
|
+
if (requestedNonce && !/^[A-Za-z0-9-]{16,128}$/.test(requestedNonce)) {
|
|
2466
|
+
throw new Error("--dev-nonce must contain 16-128 letters, numbers, or hyphens");
|
|
2467
|
+
}
|
|
2468
|
+
return { port, nonce: requestedNonce || randomUUID() };
|
|
2469
|
+
}
|
|
2282
2470
|
async function cmdLogin(flags) {
|
|
2283
2471
|
// Manual path (also the headless path): secrets supplied as flags.
|
|
2284
2472
|
if (typeof flags.token === "string") {
|
|
@@ -2291,20 +2479,51 @@ async function cmdLogin(flags) {
|
|
|
2291
2479
|
process.exitCode = 1;
|
|
2292
2480
|
return ok;
|
|
2293
2481
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2482
|
+
const store = new KeyStore();
|
|
2483
|
+
if (!flags.force && store.getToken() && store.getKey()) {
|
|
2484
|
+
console.log("This device is already connected. Run `echomem-mcp login --force` to sign in with a different account.");
|
|
2485
|
+
return true;
|
|
2486
|
+
}
|
|
2487
|
+
// Browser path: this bridge does only account/device authentication. It intentionally exposes
|
|
2488
|
+
// no local-history routes; `init` owns scan consent, reporting, and optional extraction.
|
|
2296
2489
|
console.log("Opening your browser to connect this device locally…");
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2490
|
+
const { port, nonce } = localBridgeOptions(flags);
|
|
2491
|
+
const srv = await startCallbackServer({ port, nonce, flow: "login" });
|
|
2492
|
+
const localLoginUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
2493
|
+
openBrowser(localLoginUrl);
|
|
2494
|
+
console.log(`If it didn't open, visit:\n ${localLoginUrl}\n`);
|
|
2495
|
+
console.log("Waiting for local login for up to 15 minutes…");
|
|
2496
|
+
try {
|
|
2497
|
+
const credentials = await srv.onToken;
|
|
2498
|
+
const ok = await verifyAndPrint(credentials);
|
|
2499
|
+
srv.close();
|
|
2500
|
+
if (!ok) {
|
|
2501
|
+
process.exitCode = 1;
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
console.log("✅ This device is connected. Run `echomem-mcp init` to begin local-history onboarding.");
|
|
2505
|
+
return true;
|
|
2300
2506
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2507
|
+
catch (error) {
|
|
2508
|
+
srv.close();
|
|
2509
|
+
console.error(`❌ ${error instanceof Error ? error.message : String(error)}. Run \`echomem-mcp login\` to retry.`);
|
|
2510
|
+
process.exitCode = 1;
|
|
2511
|
+
return false;
|
|
2306
2512
|
}
|
|
2307
|
-
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* The local-history onboarding flow. Existing device credentials are reused when available; a
|
|
2516
|
+
* fresh device stays in this same bridge and asks for login only after permission and report.
|
|
2517
|
+
*/
|
|
2518
|
+
async function cmdOnboarding(flags) {
|
|
2519
|
+
const store = new KeyStore();
|
|
2520
|
+
const savedToken = store.getToken();
|
|
2521
|
+
const savedKey = store.getKey();
|
|
2522
|
+
const initialToken = savedToken && savedKey
|
|
2523
|
+
? { token: savedToken, key: savedKey }
|
|
2524
|
+
: undefined;
|
|
2525
|
+
console.log("Opening your browser for EchoMem onboarding…");
|
|
2526
|
+
const { port, nonce } = localBridgeOptions(flags);
|
|
2308
2527
|
let stats = null;
|
|
2309
2528
|
let forensicReport = null;
|
|
2310
2529
|
let forensicConsent = "pending";
|
|
@@ -2323,8 +2542,10 @@ async function cmdLogin(flags) {
|
|
|
2323
2542
|
updatedAt: forensicStartedAt,
|
|
2324
2543
|
};
|
|
2325
2544
|
const srv = await startCallbackServer({
|
|
2326
|
-
port
|
|
2545
|
+
port,
|
|
2327
2546
|
nonce,
|
|
2547
|
+
flow: "onboarding",
|
|
2548
|
+
initialToken,
|
|
2328
2549
|
requireReportConsent: true,
|
|
2329
2550
|
getStats: () => stats,
|
|
2330
2551
|
getReport: () => forensicReport,
|
|
@@ -2368,7 +2589,7 @@ async function cmdLogin(flags) {
|
|
|
2368
2589
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
2369
2590
|
openBrowser(localSetupUrl);
|
|
2370
2591
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
2371
|
-
console.log("Waiting for local
|
|
2592
|
+
console.log("Waiting for local-history onboarding for up to 15 minutes…");
|
|
2372
2593
|
const startForensicScan = () => {
|
|
2373
2594
|
if (forensicScanStarted || forensicConsent !== "allowed")
|
|
2374
2595
|
return;
|
|
@@ -3116,15 +3337,16 @@ function cmdLogout() {
|
|
|
3116
3337
|
const HELP = `EchoMem MCP — local memory bridge
|
|
3117
3338
|
|
|
3118
3339
|
Usage:
|
|
3119
|
-
echomem-mcp init One command: configure
|
|
3340
|
+
echomem-mcp init One command: configure agents + HUD + login + local-history onboarding
|
|
3120
3341
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
3121
|
-
echomem-mcp setup [--client X] Detect editor, write its MCP config, then
|
|
3342
|
+
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
3122
3343
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
3123
3344
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
3124
|
-
echomem-mcp update --all
|
|
3345
|
+
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
3125
3346
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
3126
3347
|
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
3127
|
-
echomem-mcp login Connect this device
|
|
3348
|
+
echomem-mcp login Connect this device only; never scans or imports local history
|
|
3349
|
+
echomem-mcp login --force Reconnect this device with a different account
|
|
3128
3350
|
echomem-mcp unlock Privately unlock the vault on this trusted device
|
|
3129
3351
|
echomem-mcp lock Remove the local vault key while keeping the device login
|
|
3130
3352
|
echomem-mcp status Show token/key/clients
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.22",
|
|
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",
|