@echomem/mcp 1.4.14 → 1.4.16

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.
@@ -59,7 +59,73 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
59
59
  }
60
60
  .primary { background: var(--echo-ink-primary); color: #fff; }
61
61
  .secondary { background: #fff; color: var(--echo-ink-primary); }
62
+ .textButton {
63
+ min-height: 36px;
64
+ border-color: transparent;
65
+ background: transparent;
66
+ color: var(--echo-ink-faint);
67
+ padding: 0 8px;
68
+ font-size: 13px;
69
+ font-weight: 700;
70
+ }
71
+ .textButton:hover { color: var(--echo-ink-primary); background: transparent; }
62
72
  button:disabled { cursor: not-allowed; opacity: 0.55; }
73
+ .consentStage {
74
+ display: grid;
75
+ place-items: center;
76
+ min-height: min(620px, calc(100vh - 180px));
77
+ }
78
+ .localConsent {
79
+ width: min(680px, 100%);
80
+ border: 2px solid var(--echo-ink-primary);
81
+ border-radius: 8px;
82
+ background: var(--echo-paper-white);
83
+ padding: clamp(22px, 4vw, 34px);
84
+ box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45);
85
+ }
86
+ .consentEyebrow {
87
+ margin: 0 0 10px;
88
+ font-family: var(--echo-font-mono);
89
+ font-size: 11px;
90
+ font-weight: 800;
91
+ letter-spacing: 0.08em;
92
+ text-transform: uppercase;
93
+ color: var(--echo-ink-primary);
94
+ }
95
+ .localConsent h2 {
96
+ max-width: 620px;
97
+ margin: 0;
98
+ font-size: clamp(30px, 3.4vw, 42px);
99
+ line-height: 1.04;
100
+ color: var(--echo-ink-primary);
101
+ }
102
+ .localConsent > p:not(.consentEyebrow):not(.consentFine) {
103
+ max-width: 590px;
104
+ margin: 14px 0 0;
105
+ font-size: 16px;
106
+ line-height: 1.55;
107
+ color: var(--echo-ink-mute);
108
+ }
109
+ .consentActions {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ gap: 10px;
113
+ align-items: center;
114
+ margin-top: 22px;
115
+ }
116
+ .consentActions button {
117
+ min-height: 48px;
118
+ padding: 0 18px;
119
+ }
120
+ .consentFine {
121
+ min-height: 18px;
122
+ margin: 10px 0 0;
123
+ font-family: var(--echo-font-mono);
124
+ font-size: 11px;
125
+ line-height: 1.45;
126
+ color: var(--echo-ink-faint);
127
+ }
128
+ .consentFine:empty { display: none; }
63
129
  .track { height: 12px; overflow: hidden; border-radius: 999px; background: #e8e8e1; margin: 16px 0 10px; }
64
130
  .fill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 180ms ease; }
65
131
  /* indeterminate "we're working on it" bar for the prep phase (before job counts move) */
@@ -93,6 +159,7 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
93
159
  .explainCopy h2 { font-size: 38px; }
94
160
  .explainCta .promiseActions { width: 100%; }
95
161
  .explainCta .primary, .explainCta .secondary { width: 100%; }
162
+ .consentActions .primary { width: 100%; }
96
163
  .cityHero { min-height: auto; }
97
164
  .cityShell {
98
165
  width: 100vw;
@@ -615,6 +682,150 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
615
682
  .exCountdown strong { color: var(--echo-ink-primary); font-weight: 700; }
616
683
  .exOverdue { margin: 0; max-width: 620px; border: 1px solid rgba(196, 148, 42, 0.45); background: rgba(233, 185, 73, 0.12); border-radius: 8px; padding: 12px 14px; font-size: 13px; line-height: 1.5; color: var(--echo-ink-text); }
617
684
  .exOverdue strong { display: block; margin-bottom: 4px; color: var(--echo-ink-primary); }
685
+ .is-hidden { display: none !important; }
686
+ .recallGateBlock { width: min(680px, 100%); margin: 2px auto 0; }
687
+ .recallGate {
688
+ width: 100%;
689
+ position: relative;
690
+ overflow: hidden;
691
+ border: 1px solid rgba(33,64,154,0.42);
692
+ border-radius: 12px;
693
+ background:
694
+ linear-gradient(180deg, rgba(239,244,255,0.98) 0%, rgba(255,255,255,0.99) 46%),
695
+ var(--echo-paper-white);
696
+ padding: clamp(24px, 4vw, 36px);
697
+ box-shadow: 0 28px 60px -42px rgba(33,64,154,0.48);
698
+ text-align: left;
699
+ }
700
+ .recallGate::before {
701
+ content: "";
702
+ position: absolute;
703
+ inset: 0 0 auto 0;
704
+ height: 8px;
705
+ background: linear-gradient(90deg, #18317d, #4b6bd4, var(--echo-ink-primary));
706
+ }
707
+ .recallEyebrow {
708
+ display: inline-flex;
709
+ align-items: center;
710
+ gap: 8px;
711
+ margin: 0 0 14px;
712
+ border: 1px solid rgba(33,64,154,0.18);
713
+ border-radius: 999px;
714
+ background: #eef3ff;
715
+ padding: 7px 11px;
716
+ font-family: var(--echo-font-mono);
717
+ font-size: 11px;
718
+ font-weight: 900;
719
+ letter-spacing: 0.06em;
720
+ text-transform: uppercase;
721
+ color: #18317d;
722
+ }
723
+ .recallEyebrow i {
724
+ width: 8px;
725
+ height: 8px;
726
+ border-radius: 999px;
727
+ background: var(--echo-ink-primary);
728
+ box-shadow: 0 0 0 4px rgba(33,64,154,0.1);
729
+ }
730
+ .recallGate h3 {
731
+ margin: 0;
732
+ max-width: 520px;
733
+ font-family: var(--echo-font-brand);
734
+ font-size: clamp(34px, 4.6vw, 48px);
735
+ line-height: 1.02;
736
+ font-weight: 900;
737
+ color: var(--echo-ink-text);
738
+ letter-spacing: 0;
739
+ }
740
+ .recallGate p {
741
+ max-width: 560px;
742
+ margin: 14px 0 0;
743
+ font-size: 16px;
744
+ line-height: 1.55;
745
+ color: var(--echo-ink-mute);
746
+ }
747
+ .recallTrial {
748
+ margin-top: 18px;
749
+ display: inline-grid;
750
+ grid-template-columns: auto 1fr;
751
+ gap: 10px;
752
+ align-items: center;
753
+ border: 1px solid rgba(33,64,154,0.18);
754
+ border-radius: 10px;
755
+ background: #f3f6ff;
756
+ padding: 10px 12px;
757
+ color: #18317d;
758
+ }
759
+ .recallTrial strong {
760
+ font-size: 28px;
761
+ line-height: 1;
762
+ color: #18317d;
763
+ }
764
+ .recallTrial span {
765
+ font-size: 13px;
766
+ line-height: 1.25;
767
+ font-weight: 800;
768
+ }
769
+ .recallUsedNote {
770
+ margin-top: 18px;
771
+ border-left: 4px solid var(--echo-ink-primary);
772
+ background: #f3f6ff;
773
+ padding: 11px 13px;
774
+ color: #18317d;
775
+ font-size: 13px;
776
+ line-height: 1.45;
777
+ font-weight: 700;
778
+ }
779
+ .recallGateActions {
780
+ display: flex;
781
+ flex-wrap: wrap;
782
+ gap: 14px;
783
+ align-items: center;
784
+ margin-top: 24px;
785
+ }
786
+ .recallGateActions button {
787
+ min-height: 50px;
788
+ }
789
+ .recallGateActions .primary {
790
+ min-width: 0;
791
+ border-color: #18317d;
792
+ background: var(--echo-ink-primary);
793
+ color: #fff;
794
+ box-shadow: 0 18px 34px -26px rgba(33,64,154,0.85);
795
+ }
796
+ .recallGateActions .primary:hover {
797
+ background: #18317d;
798
+ }
799
+ .recallGate .recallFine {
800
+ margin-top: 10px;
801
+ font-family: var(--echo-font-mono);
802
+ font-size: 11px;
803
+ color: var(--echo-ink-faint);
804
+ }
805
+ @media (max-width: 720px) {
806
+ .recallGateActions .primary { width: 100%; }
807
+ }
808
+ .recallGate code,
809
+ .recallSkipNote code {
810
+ font-family: var(--echo-font-mono);
811
+ font-size: 0.92em;
812
+ background: var(--echo-paper-soft);
813
+ border: 1px solid var(--echo-line-soft);
814
+ border-radius: 5px;
815
+ padding: 1px 5px;
816
+ color: var(--echo-ink-primary);
817
+ }
818
+ .recallTryWrap { width: 100%; }
819
+ .recallSkipNote {
820
+ margin: 0 0 12px;
821
+ border: 1px solid var(--echo-line-soft);
822
+ border-radius: 8px;
823
+ background: rgba(246,244,238,0.86);
824
+ padding: 10px 12px;
825
+ font-size: 13px;
826
+ line-height: 1.45;
827
+ color: var(--echo-ink-mute);
828
+ }
618
829
  .tryCard { margin-top: 2px; max-width: 760px; border: 2px solid var(--echo-ink-primary); border-radius: 8px; background: var(--echo-paper-white); padding: clamp(18px, 3vw, 26px); box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45); }
619
830
  .tryCard .tryK { margin: 0 0 10px; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-primary); }
620
831
  .tryCard .tryTitle {
package/dist/setup.js CHANGED
@@ -38,6 +38,7 @@ import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "
38
38
  // device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
39
39
  const WEB_URL = (process.env.ECHO_WEB_URL || "https://yeahecho.com").replace(/\/$/, "");
40
40
  const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
41
+ const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
41
42
  function home(...p) {
42
43
  return path.join(os.homedir(), ...p);
43
44
  }
@@ -1250,6 +1251,33 @@ export function startCallbackServer(opts = {}) {
1250
1251
  json(res, 200, payload);
1251
1252
  return;
1252
1253
  }
1254
+ if (route === "/billing-status" && req.method === "GET") {
1255
+ if (!checkNonce(url.searchParams.get("nonce") || undefined))
1256
+ return void text(res, 403, "bad nonce");
1257
+ const token = new KeyStore().getToken();
1258
+ const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
1259
+ if (!token) {
1260
+ json(res, 200, { plan: "free", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
1261
+ return;
1262
+ }
1263
+ try {
1264
+ const response = await authedAxios(token).get("/api/extension/account/bootstrap", { timeout: 6000 });
1265
+ const plan = (asString(response.data?.plan) || "free").toLowerCase();
1266
+ const trialUsed = response.data?.billing?.trialUsed === true;
1267
+ const trialAvailable = response.data?.billing?.trialAvailable !== false;
1268
+ json(res, 200, {
1269
+ plan,
1270
+ paid: ["pro", "power", "team", "enterprise"].includes(plan),
1271
+ trialAvailable,
1272
+ trialUsed,
1273
+ pricingUrl,
1274
+ });
1275
+ }
1276
+ catch {
1277
+ json(res, 200, { plan: "unknown", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
1278
+ }
1279
+ return;
1280
+ }
1253
1281
  if (route === "/report" && req.method === "GET") {
1254
1282
  // Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
1255
1283
  res.setHeader("Cache-Control", "no-store");
@@ -1353,6 +1381,22 @@ export function startCallbackServer(opts = {}) {
1353
1381
  json(res, 200, progress);
1354
1382
  return;
1355
1383
  }
1384
+ if (route === "/report-consent" && req.method === "POST") {
1385
+ let body;
1386
+ try {
1387
+ body = await readJsonBody(req);
1388
+ }
1389
+ catch {
1390
+ text(res, 400, "bad json");
1391
+ return;
1392
+ }
1393
+ if (!checkNonce(asString(body.nonce)))
1394
+ return void text(res, 403, "bad nonce");
1395
+ const allowed = body.allowed === true;
1396
+ opts.onReportConsent?.(allowed);
1397
+ json(res, 200, { ok: true, allowed });
1398
+ return;
1399
+ }
1356
1400
  if (route === "/logout" && req.method === "POST") {
1357
1401
  let body;
1358
1402
  try {
@@ -1780,6 +1824,8 @@ async function cmdLogin(flags) {
1780
1824
  const nonce = devNonce || randomUUID();
1781
1825
  let stats = null;
1782
1826
  let forensicReport = null;
1827
+ let forensicConsent = "pending";
1828
+ let forensicScanStarted = false;
1783
1829
  const forensicStartedAt = Date.now();
1784
1830
  let forensicStageStartedAt = forensicStartedAt;
1785
1831
  let forensicStage = "starting";
@@ -1799,6 +1845,28 @@ async function cmdLogin(flags) {
1799
1845
  getStats: () => stats,
1800
1846
  getReport: () => forensicReport,
1801
1847
  getReportProgress: () => forensicProgress,
1848
+ onReportConsent: (allowed) => {
1849
+ if (!allowed) {
1850
+ forensicConsent = "declined";
1851
+ forensicProgress = {
1852
+ status: "failed",
1853
+ scanned: 0,
1854
+ total: 0,
1855
+ stage: "failed",
1856
+ label: "Local scan skipped",
1857
+ elapsedMs: Date.now() - forensicStartedAt,
1858
+ stageElapsedMs: Date.now() - forensicStageStartedAt,
1859
+ updatedAt: Date.now(),
1860
+ error: {
1861
+ code: "REPORT_SCAN_DECLINED",
1862
+ message: "Local file analysis was skipped. EchoMem can still connect, save conversations, and import memories.",
1863
+ },
1864
+ };
1865
+ return;
1866
+ }
1867
+ forensicConsent = "allowed";
1868
+ startForensicScan();
1869
+ },
1802
1870
  });
1803
1871
  const callbackUrl = `http://127.0.0.1:${srv.port}/callback`;
1804
1872
  const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
@@ -1812,46 +1880,50 @@ async function cmdLogin(flags) {
1812
1880
  openBrowser(localSetupUrl);
1813
1881
  console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
1814
1882
  console.log("Waiting for browser approval for up to 15 minutes…");
1815
- // Scan-first: build the local forensic "Context Doctor" report off-thread so the page shows it
1816
- // BEFORE the user connects an account (the scan is local-only; nothing leaves the machine).
1817
- buildForensicReportOffThread((progress) => {
1818
- const now = Date.now();
1819
- const nextStage = progress.stage || forensicStage;
1820
- if (nextStage !== forensicStage) {
1821
- forensicStage = nextStage;
1822
- forensicStageStartedAt = now;
1823
- console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
1824
- }
1825
- forensicProgress = {
1826
- status: "running",
1827
- scanned: progress.done,
1828
- total: progress.total,
1829
- stage: forensicStage,
1830
- label: forensicStageLabel(forensicStage),
1831
- detail: progress.detail,
1832
- elapsedMs: now - forensicStartedAt,
1833
- stageElapsedMs: now - forensicStageStartedAt,
1834
- updatedAt: now,
1835
- };
1836
- })
1837
- .then((r) => {
1838
- forensicReport = r;
1839
- })
1840
- .catch((e) => {
1841
- const now = Date.now();
1842
- forensicProgress = {
1843
- status: "failed",
1844
- scanned: forensicProgress.scanned,
1845
- total: forensicProgress.total,
1846
- stage: "failed",
1847
- label: "Local scan failed",
1848
- elapsedMs: now - forensicStartedAt,
1849
- stageElapsedMs: now - forensicStageStartedAt,
1850
- updatedAt: now,
1851
- error: safeForensicError(e),
1852
- };
1853
- console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
1854
- });
1883
+ const startForensicScan = () => {
1884
+ if (forensicScanStarted || forensicConsent !== "allowed")
1885
+ return;
1886
+ forensicScanStarted = true;
1887
+ // Build the local forensic "Context Doctor" report off-thread only after explicit consent.
1888
+ buildForensicReportOffThread((progress) => {
1889
+ const now = Date.now();
1890
+ const nextStage = progress.stage || forensicStage;
1891
+ if (nextStage !== forensicStage) {
1892
+ forensicStage = nextStage;
1893
+ forensicStageStartedAt = now;
1894
+ console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
1895
+ }
1896
+ forensicProgress = {
1897
+ status: "running",
1898
+ scanned: progress.done,
1899
+ total: progress.total,
1900
+ stage: forensicStage,
1901
+ label: forensicStageLabel(forensicStage),
1902
+ detail: progress.detail,
1903
+ elapsedMs: now - forensicStartedAt,
1904
+ stageElapsedMs: now - forensicStageStartedAt,
1905
+ updatedAt: now,
1906
+ };
1907
+ })
1908
+ .then((r) => {
1909
+ forensicReport = r;
1910
+ })
1911
+ .catch((e) => {
1912
+ const now = Date.now();
1913
+ forensicProgress = {
1914
+ status: "failed",
1915
+ scanned: forensicProgress.scanned,
1916
+ total: forensicProgress.total,
1917
+ stage: "failed",
1918
+ label: "Local scan failed",
1919
+ elapsedMs: now - forensicStartedAt,
1920
+ stageElapsedMs: now - forensicStageStartedAt,
1921
+ updatedAt: now,
1922
+ error: safeForensicError(e),
1923
+ };
1924
+ console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
1925
+ });
1926
+ };
1855
1927
  let token;
1856
1928
  let key;
1857
1929
  try {
@@ -108,6 +108,7 @@ export function listToolSpecs(opts = {}) {
108
108
  const currentTime = new Date().toISOString();
109
109
  const map = opts.map?.trim();
110
110
  const updateNotice = opts.updateNotice?.trim();
111
+ const paidRecallNote = "Requires Echo Pro or Power; saving conversations and memory extraction remain included.";
111
112
  const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
112
113
  const mapSection = map
113
114
  ? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
@@ -115,7 +116,7 @@ export function listToolSpecs(opts = {}) {
115
116
  return [
116
117
  {
117
118
  name: canonicalToolNames.search,
118
- 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.${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
119
+ 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. ${paidRecallNote}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
119
120
  inputSchema: {
120
121
  type: "object",
121
122
  properties: {
@@ -138,7 +139,7 @@ export function listToolSpecs(opts = {}) {
138
139
  },
139
140
  {
140
141
  name: "search_memories_by_description_semantic",
141
- description: "Legacy alias for search_memories.",
142
+ description: `Legacy alias for search_memories. ${paidRecallNote}`,
142
143
  inputSchema: {
143
144
  type: "object",
144
145
  properties: {
@@ -161,7 +162,7 @@ export function listToolSpecs(opts = {}) {
161
162
  },
162
163
  {
163
164
  name: canonicalToolNames.save,
164
- description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). passthrough=true stores the text verbatim as a session capsule instead reload it later via get_memories_by_context.",
165
+ description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). Saving is included. passthrough=true stores the text verbatim as a session capsule; reloading/searching saved memories requires Echo Pro or Power.",
165
166
  inputSchema: {
166
167
  type: "object",
167
168
  properties: {
@@ -194,7 +195,7 @@ export function listToolSpecs(opts = {}) {
194
195
  },
195
196
  {
196
197
  name: canonicalToolNames.timeRange,
197
- description: `Retrieve memories within a specific date range. Current time: ${currentTime}.`,
198
+ description: `Retrieve memories within a specific date range. ${paidRecallNote} Current time: ${currentTime}.`,
198
199
  inputSchema: {
199
200
  type: "object",
200
201
  properties: {
@@ -212,7 +213,7 @@ export function listToolSpecs(opts = {}) {
212
213
  },
213
214
  {
214
215
  name: canonicalToolNames.keywords,
215
- description: "Search memories based on keywords in keys field.",
216
+ description: `Search memories based on keywords in keys field. ${paidRecallNote}`,
216
217
  inputSchema: {
217
218
  type: "object",
218
219
  properties: {
@@ -369,7 +370,7 @@ export function listToolSpecs(opts = {}) {
369
370
  },
370
371
  {
371
372
  name: canonicalToolNames.getByContext,
372
- description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
373
+ description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${paidRecallNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
373
374
  inputSchema: {
374
375
  type: "object",
375
376
  properties: {
@@ -386,7 +387,7 @@ export function listToolSpecs(opts = {}) {
386
387
  },
387
388
  {
388
389
  name: canonicalToolNames.checkpointByContext,
389
- description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. Current time: ${currentTime}.`),
390
+ description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${paidRecallNote} Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. Current time: ${currentTime}.`),
390
391
  inputSchema: {
391
392
  type: "object",
392
393
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.14",
3
+ "version": "1.4.16",
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",