@echomem/mcp 1.4.21 → 1.4.23
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/hud/web.js +3 -3
- package/dist/index.js +264 -7
- package/dist/migrate.js +6 -5
- package/dist/package-metadata.js +2 -0
- package/dist/setup-page/client-core.js +144 -48
- package/dist/setup-page/client-extraction.js +260 -70
- 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 +1079 -0
- package/dist/setup-page/styles-website-alignment.js +237 -5
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +22 -6
- package/dist/setup.js +272 -40
- package/dist/v1-contract.js +226 -11
- 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));
|
|
@@ -322,6 +391,15 @@ function echomemGuidanceBlock() {
|
|
|
322
391
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
323
392
|
"- When meaningful work wraps up (a decision, a fix, a milestone) or the user asks to remember something: call `save_conversation`.",
|
|
324
393
|
"- 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.",
|
|
394
|
+
"",
|
|
395
|
+
"### Company group memory",
|
|
396
|
+
"- Use `get_group_context` when the user asks who is in their company group, what teammates are responsible for, or what work is already covered. Treat declared participant fields as facts and published-memory conclusions as evidence or inference.",
|
|
397
|
+
"- A group publication is separate from a globally public memory: publishing to a group creates a group-scoped snapshot and must not change the encrypted original or its global `is_public` setting.",
|
|
398
|
+
"- 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.",
|
|
399
|
+
"- 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.",
|
|
400
|
+
"- 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-write preview. For encrypted accounts, tell the user to run `echomem-mcp unlock` locally if the tool reports that the key is required.",
|
|
401
|
+
"- After preparing, select only exact candidate memory IDs that match the user's stated scope, exclude already-published or exact-content duplicates, summarize what would be shared, and ask the user to confirm. Do not call `complete_group_publication` until the user explicitly confirms that preview.",
|
|
402
|
+
"- On confirmation, call `complete_group_publication` with the exact `scanId`, selected memory IDs, and `confirmed: true`. An explicit request to join and upload still requires the publication preview and confirmation after joining.",
|
|
325
403
|
"- 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`.",
|
|
326
404
|
"- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
|
|
327
405
|
AGENTS_MD_END,
|
|
@@ -1201,6 +1279,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1201
1279
|
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
1202
1280
|
const expectedNonce = opts.nonce;
|
|
1203
1281
|
const scanId = opts.scanId ?? randomUUID();
|
|
1282
|
+
const flow = opts.flow ?? "onboarding";
|
|
1283
|
+
const isLoginFlow = flow === "login";
|
|
1284
|
+
// A login screen must not be blocked by a local-history permission. That permission belongs to
|
|
1285
|
+
// onboarding and is intentionally enforced separately below.
|
|
1286
|
+
const requiresReportConsent = opts.requireReportConsent === true && !isLoginFlow;
|
|
1204
1287
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
1205
1288
|
const onToken = deferred();
|
|
1206
1289
|
const decision = deferred();
|
|
@@ -1208,9 +1291,10 @@ export function startCallbackServer(opts = {}) {
|
|
|
1208
1291
|
let stats = null;
|
|
1209
1292
|
let authUrl = "";
|
|
1210
1293
|
let switchAccountUrl = "";
|
|
1211
|
-
let connected =
|
|
1294
|
+
let connected = Boolean(opts.initialToken?.token);
|
|
1295
|
+
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1212
1296
|
let pendingLocalAuth = null;
|
|
1213
|
-
let reportConsentGranted =
|
|
1297
|
+
let reportConsentGranted = !requiresReportConsent;
|
|
1214
1298
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
1215
1299
|
let migrateStarted = false;
|
|
1216
1300
|
let tokenRefreshHandler = null;
|
|
@@ -1277,7 +1361,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1277
1361
|
const handleCallback = (res, token, key, nonce) => {
|
|
1278
1362
|
if (!checkNonce(nonce))
|
|
1279
1363
|
return void text(res, 403, "bad nonce");
|
|
1280
|
-
if (
|
|
1364
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1281
1365
|
return void json(res, 403, {
|
|
1282
1366
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1283
1367
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1288,6 +1372,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1288
1372
|
console.log(`[${new Date().toISOString()}] Device token callback received.`);
|
|
1289
1373
|
const firstToken = !onToken.settled();
|
|
1290
1374
|
connected = true;
|
|
1375
|
+
activeDeviceToken = token;
|
|
1291
1376
|
const setupPath = `/setup?nonce=${encodeURIComponent(nonce || "")}&connected=1`;
|
|
1292
1377
|
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
1378
|
const callbackToken = { token, key };
|
|
@@ -1306,7 +1391,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1306
1391
|
text(res, 403, "bad nonce");
|
|
1307
1392
|
return true;
|
|
1308
1393
|
}
|
|
1309
|
-
if (
|
|
1394
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1310
1395
|
json(res, 403, {
|
|
1311
1396
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1312
1397
|
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
@@ -1318,6 +1403,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1318
1403
|
const resolveLocalToken = (token, key) => {
|
|
1319
1404
|
const firstToken = !onToken.settled();
|
|
1320
1405
|
connected = true;
|
|
1406
|
+
activeDeviceToken = token;
|
|
1321
1407
|
pendingLocalAuth = null;
|
|
1322
1408
|
const callbackToken = { token, key };
|
|
1323
1409
|
if (firstToken) {
|
|
@@ -1330,6 +1416,17 @@ export function startCallbackServer(opts = {}) {
|
|
|
1330
1416
|
}
|
|
1331
1417
|
armTimeout();
|
|
1332
1418
|
};
|
|
1419
|
+
const isOnboardingOnlyRoute = (route) => [
|
|
1420
|
+
"/report-consent",
|
|
1421
|
+
"/report",
|
|
1422
|
+
"/stats",
|
|
1423
|
+
"/billing-status",
|
|
1424
|
+
"/billing-checkout",
|
|
1425
|
+
"/billing-portal",
|
|
1426
|
+
"/progress",
|
|
1427
|
+
"/migrate",
|
|
1428
|
+
"/skip",
|
|
1429
|
+
].includes(route);
|
|
1333
1430
|
const handleLocalSendOtp = async (res, body) => {
|
|
1334
1431
|
if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
|
|
1335
1432
|
return;
|
|
@@ -1472,6 +1569,15 @@ export function startCallbackServer(opts = {}) {
|
|
|
1472
1569
|
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
1473
1570
|
const route = url.pathname;
|
|
1474
1571
|
const run = async () => {
|
|
1572
|
+
// Defense in depth: the login bridge never exposes the routes that can inspect or move
|
|
1573
|
+
// local conversation history. The browser UI also routes around them, but the server is
|
|
1574
|
+
// the authority for this privacy boundary.
|
|
1575
|
+
if (isLoginFlow && isOnboardingOnlyRoute(route)) {
|
|
1576
|
+
return void json(res, 404, {
|
|
1577
|
+
error: "ONBOARDING_REQUIRED",
|
|
1578
|
+
message: "Run `echomem-mcp init` to access local-history onboarding.",
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1475
1581
|
if ((route === "/city" || route.startsWith("/city/")) && req.method === "GET") {
|
|
1476
1582
|
serveRepoCityAsset(route, res);
|
|
1477
1583
|
return;
|
|
@@ -1507,12 +1613,13 @@ export function startCallbackServer(opts = {}) {
|
|
|
1507
1613
|
return void text(res, 403, "bad nonce");
|
|
1508
1614
|
json(res, 200, {
|
|
1509
1615
|
connected,
|
|
1616
|
+
flow,
|
|
1510
1617
|
authUrl,
|
|
1511
1618
|
switchAccountUrl: switchAccountUrl || authUrl,
|
|
1512
1619
|
localOnly: true,
|
|
1513
1620
|
localAuth: true,
|
|
1514
1621
|
workspacePath: process.cwd(),
|
|
1515
|
-
consentRequired:
|
|
1622
|
+
consentRequired: requiresReportConsent,
|
|
1516
1623
|
consentGranted: reportConsentGranted,
|
|
1517
1624
|
});
|
|
1518
1625
|
return;
|
|
@@ -1605,7 +1712,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1605
1712
|
if (route === "/stats" && req.method === "GET") {
|
|
1606
1713
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1607
1714
|
return void text(res, 403, "bad nonce");
|
|
1608
|
-
if (
|
|
1715
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1609
1716
|
return void json(res, 403, {
|
|
1610
1717
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1611
1718
|
message: "Allow local history access before continuing setup.",
|
|
@@ -1618,15 +1725,16 @@ export function startCallbackServer(opts = {}) {
|
|
|
1618
1725
|
return;
|
|
1619
1726
|
}
|
|
1620
1727
|
if (route === "/billing-status" && req.method === "GET") {
|
|
1728
|
+
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
1621
1729
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1622
1730
|
return void text(res, 403, "bad nonce");
|
|
1623
|
-
if (
|
|
1731
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1624
1732
|
return void json(res, 403, {
|
|
1625
1733
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1626
1734
|
message: "Allow local history access before continuing setup.",
|
|
1627
1735
|
});
|
|
1628
1736
|
}
|
|
1629
|
-
const token = new KeyStore().getToken();
|
|
1737
|
+
const token = activeDeviceToken || new KeyStore().getToken();
|
|
1630
1738
|
const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
|
|
1631
1739
|
if (!token) {
|
|
1632
1740
|
json(res, 200, { plan: "free", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
|
|
@@ -1662,12 +1770,84 @@ export function startCallbackServer(opts = {}) {
|
|
|
1662
1770
|
}
|
|
1663
1771
|
return;
|
|
1664
1772
|
}
|
|
1773
|
+
if ((route === "/billing-checkout" || route === "/billing-portal") && req.method === "POST") {
|
|
1774
|
+
let body;
|
|
1775
|
+
try {
|
|
1776
|
+
body = await readJsonBody(req);
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
text(res, 400, "bad json");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (!checkNonce(asString(body.nonce)))
|
|
1783
|
+
return void text(res, 403, "bad nonce");
|
|
1784
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1785
|
+
return void json(res, 403, {
|
|
1786
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1787
|
+
message: "Allow local history access before managing an onboarding plan.",
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
const token = activeDeviceToken || new KeyStore().getToken();
|
|
1791
|
+
if (!connected || !token) {
|
|
1792
|
+
return void json(res, 401, {
|
|
1793
|
+
error: "ECHOMEM_LOGIN_REQUIRED",
|
|
1794
|
+
message: "Connect your EchoMem account before continuing to billing.",
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
const isCheckout = route === "/billing-checkout";
|
|
1798
|
+
const requestBody = { source: "mcp_onboarding" };
|
|
1799
|
+
if (isCheckout) {
|
|
1800
|
+
const plan = asString(body.plan)?.toLowerCase();
|
|
1801
|
+
if (plan !== "pro" && plan !== "power") {
|
|
1802
|
+
return void json(res, 400, {
|
|
1803
|
+
error: "INVALID_PLAN",
|
|
1804
|
+
message: "Choose Pro or Power to continue to checkout.",
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
requestBody.plan = plan;
|
|
1808
|
+
requestBody.trial = body.trial === true;
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
const plan = asString(body.plan)?.toLowerCase();
|
|
1812
|
+
if (plan) {
|
|
1813
|
+
if (plan !== "pro" && plan !== "power") {
|
|
1814
|
+
return void json(res, 400, {
|
|
1815
|
+
error: "INVALID_PLAN",
|
|
1816
|
+
message: "Choose Pro or Power to change plans.",
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
requestBody.plan = plan;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
const response = await axios.post(hostedBillingEndpoint(isCheckout ? "/api/billing/checkout" : "/api/billing/portal"), requestBody, {
|
|
1824
|
+
timeout: 15_000,
|
|
1825
|
+
headers: {
|
|
1826
|
+
"Content-Type": "application/json",
|
|
1827
|
+
Authorization: `Bearer ${token}`,
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
const hostedUrl = response.data?.url;
|
|
1831
|
+
if (!isExpectedStripeUrl(hostedUrl, isCheckout ? "checkout" : "portal")) {
|
|
1832
|
+
return void json(res, 502, {
|
|
1833
|
+
error: "INVALID_BILLING_DESTINATION",
|
|
1834
|
+
message: "Echo billing returned an unexpected destination.",
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
json(res, 200, { url: hostedUrl });
|
|
1838
|
+
}
|
|
1839
|
+
catch (error) {
|
|
1840
|
+
const detail = publicAxiosError(error, isCheckout ? "Could not start secure checkout." : "Could not open secure plan management.");
|
|
1841
|
+
json(res, detail.status, { error: "BILLING_REQUEST_FAILED", message: detail.message });
|
|
1842
|
+
}
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1665
1845
|
if (route === "/report" && req.method === "GET") {
|
|
1666
1846
|
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
1667
1847
|
res.setHeader("Cache-Control", "no-store");
|
|
1668
1848
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1669
1849
|
return void text(res, 403, "bad nonce");
|
|
1670
|
-
if (
|
|
1850
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1671
1851
|
return void json(res, 403, {
|
|
1672
1852
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1673
1853
|
message: "Allow local history access before starting the local scan.",
|
|
@@ -1768,7 +1948,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1768
1948
|
if (route === "/progress" && req.method === "GET") {
|
|
1769
1949
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1770
1950
|
return void text(res, 403, "bad nonce");
|
|
1771
|
-
if (
|
|
1951
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1772
1952
|
return void json(res, 403, {
|
|
1773
1953
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1774
1954
|
message: "Allow local history access before continuing setup.",
|
|
@@ -1816,6 +1996,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1816
1996
|
/* already logged out locally */
|
|
1817
1997
|
}
|
|
1818
1998
|
connected = false;
|
|
1999
|
+
activeDeviceToken = "";
|
|
1819
2000
|
const revokedPendingCredential = await revokePendingLocalAuth();
|
|
1820
2001
|
stats = null;
|
|
1821
2002
|
migrateStarted = false;
|
|
@@ -1845,7 +2026,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1845
2026
|
}
|
|
1846
2027
|
if (!checkNonce(asString(body.nonce)))
|
|
1847
2028
|
return void text(res, 403, "bad nonce");
|
|
1848
|
-
if (
|
|
2029
|
+
if (requiresReportConsent && !reportConsentGranted) {
|
|
1849
2030
|
return void json(res, 403, {
|
|
1850
2031
|
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1851
2032
|
message: "Allow local history access before starting extraction.",
|
|
@@ -1906,6 +2087,8 @@ export function startCallbackServer(opts = {}) {
|
|
|
1906
2087
|
sockets.add(socket);
|
|
1907
2088
|
socket.on("close", () => sockets.delete(socket));
|
|
1908
2089
|
});
|
|
2090
|
+
if (opts.initialToken)
|
|
2091
|
+
onToken.resolve(opts.initialToken);
|
|
1909
2092
|
armTimeout();
|
|
1910
2093
|
server.on("error", (e) => rejectOuter(e));
|
|
1911
2094
|
server.listen(opts.port ?? 0, "127.0.0.1", () => {
|
|
@@ -2140,9 +2323,9 @@ async function cmdSetup(flags) {
|
|
|
2140
2323
|
/**
|
|
2141
2324
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
2142
2325
|
* 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
|
|
2326
|
+
* Codex skills, writes the AGENTS.md memory guidance, and launches the context HUD. One browser
|
|
2327
|
+
* bridge then runs permission → report → login → plan if needed → extraction in that order.
|
|
2328
|
+
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
2146
2329
|
*/
|
|
2147
2330
|
async function cmdInit(flags) {
|
|
2148
2331
|
console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
|
|
@@ -2151,10 +2334,10 @@ async function cmdInit(flags) {
|
|
|
2151
2334
|
// 2. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
|
|
2152
2335
|
if (!flags["no-hud"])
|
|
2153
2336
|
await cmdSetupHud(flags);
|
|
2154
|
-
// 3. Start
|
|
2337
|
+
// 3. Start one ordered onboarding bridge. A fresh device logs in only after consent + report.
|
|
2155
2338
|
console.log("");
|
|
2156
|
-
if (!flags["skip-login"] && !flags["no-login"] && !await
|
|
2157
|
-
console.log("\nEchoMem is configured, but
|
|
2339
|
+
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
2340
|
+
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
2158
2341
|
return;
|
|
2159
2342
|
}
|
|
2160
2343
|
console.log("");
|
|
@@ -2215,6 +2398,8 @@ function writeCodexSkillsForTargets(targets) {
|
|
|
2215
2398
|
}
|
|
2216
2399
|
}
|
|
2217
2400
|
async function cmdUpdate(flags) {
|
|
2401
|
+
if (!flags.dev)
|
|
2402
|
+
installDurableGlobalUpdate();
|
|
2218
2403
|
await cmdSetup({ ...flags, "skip-login": true });
|
|
2219
2404
|
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
2220
2405
|
}
|
|
@@ -2279,6 +2464,19 @@ function parseHudClient(value) {
|
|
|
2279
2464
|
? value
|
|
2280
2465
|
: "auto";
|
|
2281
2466
|
}
|
|
2467
|
+
function localBridgeOptions(flags) {
|
|
2468
|
+
const port = typeof flags["dev-port"] === "string" ? Number(flags["dev-port"]) : undefined;
|
|
2469
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 1024 || port > 65535)) {
|
|
2470
|
+
throw new Error("--dev-port must be an integer between 1024 and 65535");
|
|
2471
|
+
}
|
|
2472
|
+
const requestedNonce = typeof flags["dev-nonce"] === "string" ? flags["dev-nonce"].trim() : undefined;
|
|
2473
|
+
if (requestedNonce && port === undefined)
|
|
2474
|
+
throw new Error("--dev-nonce requires --dev-port");
|
|
2475
|
+
if (requestedNonce && !/^[A-Za-z0-9-]{16,128}$/.test(requestedNonce)) {
|
|
2476
|
+
throw new Error("--dev-nonce must contain 16-128 letters, numbers, or hyphens");
|
|
2477
|
+
}
|
|
2478
|
+
return { port, nonce: requestedNonce || randomUUID() };
|
|
2479
|
+
}
|
|
2282
2480
|
async function cmdLogin(flags) {
|
|
2283
2481
|
// Manual path (also the headless path): secrets supplied as flags.
|
|
2284
2482
|
if (typeof flags.token === "string") {
|
|
@@ -2291,20 +2489,51 @@ async function cmdLogin(flags) {
|
|
|
2291
2489
|
process.exitCode = 1;
|
|
2292
2490
|
return ok;
|
|
2293
2491
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2492
|
+
const store = new KeyStore();
|
|
2493
|
+
if (!flags.force && store.getToken() && store.getKey()) {
|
|
2494
|
+
console.log("This device is already connected. Run `echomem-mcp login --force` to sign in with a different account.");
|
|
2495
|
+
return true;
|
|
2496
|
+
}
|
|
2497
|
+
// Browser path: this bridge does only account/device authentication. It intentionally exposes
|
|
2498
|
+
// no local-history routes; `init` owns scan consent, reporting, and optional extraction.
|
|
2296
2499
|
console.log("Opening your browser to connect this device locally…");
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2500
|
+
const { port, nonce } = localBridgeOptions(flags);
|
|
2501
|
+
const srv = await startCallbackServer({ port, nonce, flow: "login" });
|
|
2502
|
+
const localLoginUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
2503
|
+
openBrowser(localLoginUrl);
|
|
2504
|
+
console.log(`If it didn't open, visit:\n ${localLoginUrl}\n`);
|
|
2505
|
+
console.log("Waiting for local login for up to 15 minutes…");
|
|
2506
|
+
try {
|
|
2507
|
+
const credentials = await srv.onToken;
|
|
2508
|
+
const ok = await verifyAndPrint(credentials);
|
|
2509
|
+
srv.close();
|
|
2510
|
+
if (!ok) {
|
|
2511
|
+
process.exitCode = 1;
|
|
2512
|
+
return false;
|
|
2513
|
+
}
|
|
2514
|
+
console.log("✅ This device is connected. Run `echomem-mcp init` to begin local-history onboarding.");
|
|
2515
|
+
return true;
|
|
2300
2516
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2517
|
+
catch (error) {
|
|
2518
|
+
srv.close();
|
|
2519
|
+
console.error(`❌ ${error instanceof Error ? error.message : String(error)}. Run \`echomem-mcp login\` to retry.`);
|
|
2520
|
+
process.exitCode = 1;
|
|
2521
|
+
return false;
|
|
2306
2522
|
}
|
|
2307
|
-
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* The local-history onboarding flow. Existing device credentials are reused when available; a
|
|
2526
|
+
* fresh device stays in this same bridge and asks for login only after permission and report.
|
|
2527
|
+
*/
|
|
2528
|
+
async function cmdOnboarding(flags) {
|
|
2529
|
+
const store = new KeyStore();
|
|
2530
|
+
const savedToken = store.getToken();
|
|
2531
|
+
const savedKey = store.getKey();
|
|
2532
|
+
const initialToken = savedToken && savedKey
|
|
2533
|
+
? { token: savedToken, key: savedKey }
|
|
2534
|
+
: undefined;
|
|
2535
|
+
console.log("Opening your browser for EchoMem onboarding…");
|
|
2536
|
+
const { port, nonce } = localBridgeOptions(flags);
|
|
2308
2537
|
let stats = null;
|
|
2309
2538
|
let forensicReport = null;
|
|
2310
2539
|
let forensicConsent = "pending";
|
|
@@ -2323,8 +2552,10 @@ async function cmdLogin(flags) {
|
|
|
2323
2552
|
updatedAt: forensicStartedAt,
|
|
2324
2553
|
};
|
|
2325
2554
|
const srv = await startCallbackServer({
|
|
2326
|
-
port
|
|
2555
|
+
port,
|
|
2327
2556
|
nonce,
|
|
2557
|
+
flow: "onboarding",
|
|
2558
|
+
initialToken,
|
|
2328
2559
|
requireReportConsent: true,
|
|
2329
2560
|
getStats: () => stats,
|
|
2330
2561
|
getReport: () => forensicReport,
|
|
@@ -2368,7 +2599,7 @@ async function cmdLogin(flags) {
|
|
|
2368
2599
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
2369
2600
|
openBrowser(localSetupUrl);
|
|
2370
2601
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
2371
|
-
console.log("Waiting for local
|
|
2602
|
+
console.log("Waiting for local-history onboarding for up to 15 minutes…");
|
|
2372
2603
|
const startForensicScan = () => {
|
|
2373
2604
|
if (forensicScanStarted || forensicConsent !== "allowed")
|
|
2374
2605
|
return;
|
|
@@ -3116,15 +3347,16 @@ function cmdLogout() {
|
|
|
3116
3347
|
const HELP = `EchoMem MCP — local memory bridge
|
|
3117
3348
|
|
|
3118
3349
|
Usage:
|
|
3119
|
-
echomem-mcp init One command: configure
|
|
3350
|
+
echomem-mcp init One command: configure agents + HUD + login + local-history onboarding
|
|
3120
3351
|
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
|
|
3352
|
+
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
3122
3353
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
3123
3354
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
3124
|
-
echomem-mcp update --all
|
|
3355
|
+
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
3125
3356
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
3126
3357
|
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
3127
|
-
echomem-mcp login Connect this device
|
|
3358
|
+
echomem-mcp login Connect this device only; never scans or imports local history
|
|
3359
|
+
echomem-mcp login --force Reconnect this device with a different account
|
|
3128
3360
|
echomem-mcp unlock Privately unlock the vault on this trusted device
|
|
3129
3361
|
echomem-mcp lock Remove the local vault key while keeping the device login
|
|
3130
3362
|
echomem-mcp status Show token/key/clients
|