@echomem/mcp 1.4.15 → 1.4.17

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.
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { echoConfigDir } from "./keystore.js";
4
+ const ALERT_FILE = "billing-alert.json";
5
+ const DEFAULT_MAX_AGE_MS = 31 * 24 * 60 * 60 * 1000;
6
+ function alertPath() {
7
+ return path.join(echoConfigDir(), ALERT_FILE);
8
+ }
9
+ export function writeBillingAlert(alert) {
10
+ try {
11
+ fs.mkdirSync(echoConfigDir(), { recursive: true, mode: 0o700 });
12
+ fs.writeFileSync(alertPath(), JSON.stringify({ ...alert, updatedAt: new Date().toISOString() }, null, 2), { mode: 0o600 });
13
+ }
14
+ catch {
15
+ /* best-effort: billing UI should never break search */
16
+ }
17
+ }
18
+ export function readBillingAlert(maxAgeMs = DEFAULT_MAX_AGE_MS) {
19
+ try {
20
+ const alert = JSON.parse(fs.readFileSync(alertPath(), "utf8"));
21
+ const updatedAt = Date.parse(alert.updatedAt || "");
22
+ if (!updatedAt || Date.now() - updatedAt > maxAgeMs)
23
+ return null;
24
+ if (!alert.message || !alert.pricingUrl)
25
+ return null;
26
+ return alert;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function clearBillingAlert() {
33
+ try {
34
+ fs.unlinkSync(alertPath());
35
+ }
36
+ catch {
37
+ /* absent is fine */
38
+ }
39
+ }
@@ -30,21 +30,17 @@ function normalizePlan(value) {
30
30
  /** Parse the account bootstrap contract without importing app-only code into the npm package. */
31
31
  export function accountDiagnosticsFromBootstrap(payload) {
32
32
  const root = record(payload) ?? {};
33
- const entitlements = record(root.entitlements) ?? {};
34
33
  const usage = record(root.usage) ?? {};
35
- const activation = record(root.activation) ?? {};
36
- const limit = number(entitlements.deepSearchPerMonth);
37
- const searchesThisMonth = number(activation.memorySearchMonthToDateCount);
38
- const searchesRemaining = limit === undefined || searchesThisMonth === undefined
39
- ? undefined
40
- : limit < 0
41
- ? null
42
- : Math.max(0, limit - searchesThisMonth);
34
+ const quota = record(root.memorySearchQuota) ?? {};
35
+ const weeklySearchLimit = number(quota.limit);
36
+ const searchesThisWeek = number(quota.used);
37
+ const searchesRemaining = quota.remaining === null ? null : number(quota.remaining);
43
38
  return {
44
39
  plan: normalizePlan(root.plan),
45
- deepSearchPerMonth: limit,
46
- searchesThisMonth,
40
+ weeklySearchLimit,
41
+ searchesThisWeek,
47
42
  searchesRemaining,
43
+ resetAt: text(quota.resetAt),
48
44
  memoryCount: number(usage.memories),
49
45
  sourceCount: number(usage.sources),
50
46
  };
@@ -10,7 +10,9 @@ import { loadHudDiagnostics } from "./diagnostics.js";
10
10
  import { homePath, newestFile, walkFiles } from "./fs.js";
11
11
  import { KeyStore } from "../keystore.js";
12
12
  import { assembleCodex, assembleClaude } from "../migrate.js";
13
+ import { readBillingAlert } from "../billing-alert.js";
13
14
  const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
15
+ const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
14
16
  export async function createHudServer(opts = {}) {
15
17
  const mode = opts.mode || "auto";
16
18
  const port = opts.port ?? 17377;
@@ -43,6 +45,32 @@ export async function createHudServer(opts = {}) {
43
45
  res.end(JSON.stringify(latest, null, 2));
44
46
  return;
45
47
  }
48
+ if (url.pathname === "/billing-status") {
49
+ handleBillingStatus(res).catch((error) => {
50
+ if (res.writableEnded)
51
+ return;
52
+ res.writeHead(200, { "content-type": "application/json" });
53
+ res.end(JSON.stringify({ ok: false, state: "unknown", reason: error instanceof Error ? error.message : "billing status failed" }));
54
+ });
55
+ return;
56
+ }
57
+ if (url.pathname === "/open-external") {
58
+ const target = url.searchParams.get("url") || "";
59
+ let ok = false;
60
+ try {
61
+ const parsed = new URL(target);
62
+ if ((parsed.protocol === "https:" || parsed.protocol === "http:") && typeof opts.onOpenExternal === "function") {
63
+ opts.onOpenExternal(parsed.toString());
64
+ ok = true;
65
+ }
66
+ }
67
+ catch {
68
+ ok = false;
69
+ }
70
+ res.writeHead(200, { "content-type": "application/json" });
71
+ res.end(JSON.stringify({ ok, reason: ok ? undefined : opts.onOpenExternal ? "invalid_url" : "no_desktop" }));
72
+ return;
73
+ }
46
74
  if (url.pathname === "/diagnostics") {
47
75
  loadHudDiagnostics().then((diagnostics) => {
48
76
  if (res.writableEnded)
@@ -264,6 +292,69 @@ export async function createHudServer(opts = {}) {
264
292
  }),
265
293
  };
266
294
  }
295
+ async function handleBillingStatus(res) {
296
+ const respond = (obj) => {
297
+ if (res.writableEnded)
298
+ return;
299
+ res.writeHead(200, { "content-type": "application/json" });
300
+ res.end(JSON.stringify(obj));
301
+ };
302
+ const pricingUrl = appendSource(PRICING_URL, "hud");
303
+ const alert = readBillingAlert();
304
+ const token = new KeyStore().getToken();
305
+ if (!token) {
306
+ return respond({ ok: false, state: "not_connected", pricingUrl });
307
+ }
308
+ let plan = "unknown";
309
+ let paid = false;
310
+ try {
311
+ const response = await fetch(`${API_BASE}/api/extension/account/bootstrap`, {
312
+ headers: { Authorization: `Bearer ${token}` },
313
+ });
314
+ const data = (await response.json().catch(() => ({})));
315
+ if (response.ok) {
316
+ plan = typeof data.plan === "string" ? data.plan.toLowerCase() : "free";
317
+ paid = ["pro", "power", "team", "enterprise"].includes(plan);
318
+ }
319
+ }
320
+ catch {
321
+ /* Local alert still gives the HUD something useful. */
322
+ }
323
+ if (alert && (alert.kind === "quota_exceeded" || !paid)) {
324
+ return respond({
325
+ ok: true,
326
+ state: alert.kind,
327
+ plan: alert.plan || plan,
328
+ message: alert.kind === "quota_exceeded" ? "Recall limit reached." : "Recall needs a plan.",
329
+ detail: alert.message,
330
+ code: alert.code,
331
+ pricingUrl: appendSource(alert.pricingUrl || pricingUrl, "hud"),
332
+ updatedAt: alert.updatedAt,
333
+ });
334
+ }
335
+ if (!paid && plan === "free") {
336
+ return respond({
337
+ ok: true,
338
+ state: "plan_required",
339
+ plan,
340
+ message: "Recall needs a plan.",
341
+ detail: "Search and recall require Echo Pro or Power. Saving conversations keeps working.",
342
+ pricingUrl,
343
+ });
344
+ }
345
+ respond({ ok: true, state: "ok", plan, paid, pricingUrl });
346
+ }
347
+ function appendSource(url, source) {
348
+ try {
349
+ const parsed = new URL(url);
350
+ if (!parsed.searchParams.has("source"))
351
+ parsed.searchParams.set("source", source);
352
+ return parsed.toString();
353
+ }
354
+ catch {
355
+ return url;
356
+ }
357
+ }
267
358
  async function handleCheckpointStatus(latest, res) {
268
359
  const respond = (obj) => {
269
360
  if (res.writableEnded)
package/dist/hud/web.js CHANGED
@@ -16,6 +16,10 @@ export const HUD_HTML = String.raw `<!doctype html>
16
16
  --brick: #c7372f;
17
17
  --green: #43d17a;
18
18
  --amber: #e9b949;
19
+ --billing-free-bg: #fff8d9;
20
+ --billing-free-border: rgba(190, 128, 19, 0.36);
21
+ --billing-free: #c08412;
22
+ --billing-free-dark: #87560d;
19
23
  --line: rgba(26, 58, 143, 0.22);
20
24
  --track: rgba(26, 58, 143, 0.1);
21
25
  --shadow: drop-shadow(0 2px 7px rgba(26, 58, 143, 0.12));
@@ -530,6 +534,75 @@ export const HUD_HTML = String.raw `<!doctype html>
530
534
  background: rgba(199, 55, 47, 0.07);
531
535
  color: #7a241e;
532
536
  }
537
+ .billing-banner {
538
+ display: none;
539
+ margin: 0 0 12px;
540
+ border: 1.4px solid var(--billing-free-border);
541
+ border-radius: 12px;
542
+ background: linear-gradient(180deg, var(--billing-free-bg), #fff);
543
+ padding: 10px;
544
+ color: var(--muted);
545
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
546
+ }
547
+ .billing-banner.limit {
548
+ border-color: rgba(255, 95, 87, 0.45);
549
+ background: linear-gradient(180deg, #fff3f2, #fff);
550
+ }
551
+ .billing-top {
552
+ display: flex;
553
+ align-items: center;
554
+ gap: 8px;
555
+ }
556
+ .billing-dot {
557
+ width: 10px;
558
+ height: 10px;
559
+ flex: 0 0 auto;
560
+ border-radius: 999px;
561
+ background: var(--billing-free);
562
+ box-shadow: 0 0 0 4px rgba(190, 128, 19, 0.12);
563
+ }
564
+ .billing-banner.limit .billing-dot {
565
+ background: #ff5f57;
566
+ box-shadow: 0 0 0 4px rgba(255, 95, 87, 0.12);
567
+ }
568
+ .billing-title {
569
+ min-width: 0;
570
+ flex: 1;
571
+ color: var(--billing-free-dark);
572
+ font-size: 13px;
573
+ font-weight: 900;
574
+ line-height: 1.15;
575
+ }
576
+ .billing-banner.limit .billing-title { color: #7a241e; }
577
+ .billing-copy {
578
+ display: block;
579
+ margin: 8px 0 0 18px;
580
+ font-size: 11.5px;
581
+ line-height: 1.35;
582
+ font-weight: 650;
583
+ }
584
+ .billing-link {
585
+ display: inline-flex;
586
+ align-items: center;
587
+ justify-content: center;
588
+ min-height: 28px;
589
+ border: 1px solid var(--billing-free-dark);
590
+ border-radius: 999px;
591
+ background: var(--billing-free);
592
+ color: #fff;
593
+ padding: 0 13px;
594
+ font-family: inherit;
595
+ font-size: 11px;
596
+ font-weight: 900;
597
+ cursor: pointer;
598
+ box-shadow: 0 12px 20px -16px rgba(135, 86, 13, 0.95);
599
+ }
600
+ .billing-banner.limit .billing-link {
601
+ border-color: #7a241e;
602
+ background: #7a241e;
603
+ box-shadow: 0 12px 20px -16px rgba(122, 36, 30, 0.95);
604
+ }
605
+ .billing-link:hover { filter: brightness(0.96); }
533
606
  button.renew-btn {
534
607
  display: none;
535
608
  width: 100%;
@@ -697,6 +770,14 @@ export const HUD_HTML = String.raw `<!doctype html>
697
770
  <button class="pin-chip" id="pinchip" type="button" title="Return to auto-follow">pinned x</button>
698
771
  <button class="jump-chip" id="jumpchip" type="button" title="Open this conversation">&#8599;</button>
699
772
  </div>
773
+ <div class="billing-banner" id="billingbanner">
774
+ <div class="billing-top">
775
+ <span class="billing-dot"></span>
776
+ <strong class="billing-title" id="billingtitle"></strong>
777
+ <button class="billing-link" id="billinglink" type="button">Open pricing</button>
778
+ </div>
779
+ <span class="billing-copy" id="billingcopy"></span>
780
+ </div>
700
781
  <div class="state-row">
701
782
  <span class="state-dot" id="stateDot"></span>
702
783
  <span class="state-word" id="stateWord">--%</span>
@@ -745,6 +826,10 @@ export const HUD_HTML = String.raw `<!doctype html>
745
826
  const whyBtn = document.getElementById("whybtn");
746
827
  const evidence = document.getElementById("evidence");
747
828
  const nudge = document.getElementById("nudge");
829
+ const billingBanner = document.getElementById("billingbanner");
830
+ const billingTitle = document.getElementById("billingtitle");
831
+ const billingCopy = document.getElementById("billingcopy");
832
+ const billingLink = document.getElementById("billinglink");
748
833
  const renewBtn = document.getElementById("renewbtn");
749
834
  const renewResult = document.getElementById("renewresult");
750
835
  const renewMsg = document.getElementById("renewmsg");
@@ -773,6 +858,7 @@ export const HUD_HTML = String.raw `<!doctype html>
773
858
  let renewStartMs = 0;
774
859
  let renewEta = null;
775
860
  let renewEstimateSeq = 0;
861
+ let billingStatus = null;
776
862
  let diagnosticsLoaded = false;
777
863
  let diagnosticsInFlight = false;
778
864
  const CHECKPOINT_CACHE_KEY = "echomemHudCheckpointCacheV2";
@@ -807,12 +893,13 @@ export const HUD_HTML = String.raw `<!doctype html>
807
893
  const lines = [];
808
894
  if (account.state === "connected") {
809
895
  lines.push(diagnosticsLine("account", planLabel(account.plan) + " · authenticated"));
810
- if (typeof account.searchesThisMonth === "number" && typeof account.deepSearchPerMonth === "number") {
811
- const limit = account.deepSearchPerMonth < 0 ? "Unlimited" : String(account.deepSearchPerMonth);
896
+ if (typeof account.searchesThisWeek === "number" && typeof account.weeklySearchLimit === "number") {
897
+ const limit = account.weeklySearchLimit < 0 ? "Unlimited" : String(account.weeklySearchLimit);
812
898
  const remaining = account.searchesRemaining === null ? "" : (typeof account.searchesRemaining === "number" ? " · " + account.searchesRemaining + " left" : "");
813
- lines.push(diagnosticsLine("searches", account.searchesThisMonth + " / " + limit + " UTC month" + remaining));
899
+ lines.push(diagnosticsLine("searches", account.searchesThisWeek + " / " + limit + " UTC week" + remaining));
900
+ if (account.resetAt) lines.push(diagnosticsLine("resets", account.resetAt));
814
901
  } else {
815
- lines.push(diagnosticsLine("searches", "month-to-date not reported"));
902
+ lines.push(diagnosticsLine("searches", "weekly quota not reported"));
816
903
  }
817
904
  } else if (account.state === "not_logged_in") {
818
905
  lines.push(diagnosticsLine("account", "not logged in", true));
@@ -984,6 +1071,19 @@ export const HUD_HTML = String.raw `<!doctype html>
984
1071
  e.stopPropagation();
985
1072
  selectSession("auto");
986
1073
  });
1074
+ if (billingLink) billingLink.addEventListener("click", (e) => {
1075
+ e.stopPropagation();
1076
+ const url = billingStatus && billingStatus.pricingUrl;
1077
+ if (!url) return;
1078
+ fetch("/open-external?url=" + encodeURIComponent(url))
1079
+ .then((res) => res.json())
1080
+ .then((data) => {
1081
+ if (!data || !data.ok) window.open(url, "_blank", "noopener");
1082
+ })
1083
+ .catch(() => {
1084
+ try { window.open(url, "_blank", "noopener"); } catch {}
1085
+ });
1086
+ });
987
1087
  function selectSession(id) {
988
1088
  if (!id) return;
989
1089
  fetch("/select?id=" + encodeURIComponent(id))
@@ -1479,6 +1579,19 @@ export const HUD_HTML = String.raw `<!doctype html>
1479
1579
  const events = new EventSource("/events");
1480
1580
  events.onmessage = (event) => render(JSON.parse(event.data));
1481
1581
  fetch("/state").then((r) => r.json()).then(render).catch(() => {});
1582
+ refreshBillingStatus();
1583
+ setInterval(refreshBillingStatus, 30000);
1584
+ }
1585
+
1586
+ async function refreshBillingStatus() {
1587
+ try {
1588
+ const res = await fetch("/billing-status");
1589
+ billingStatus = await res.json();
1590
+ } catch {
1591
+ billingStatus = null;
1592
+ }
1593
+ renderBillingStatus();
1594
+ syncHeight();
1482
1595
  }
1483
1596
 
1484
1597
  function render(state) {
@@ -1536,6 +1649,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1536
1649
  jumpChip.style.display = activeId ? "inline-block" : "none";
1537
1650
  }
1538
1651
  renderWhy(score, oldPct, repeatCount, sat, health, color);
1652
+ renderBillingStatus();
1539
1653
  pathEl.textContent = "";
1540
1654
  currentSrc = score.sourcePath || null;
1541
1655
  if (revealEl) revealEl.style.display = !DEMO_CLEAN && currentSrc ? "inline-block" : "none";
@@ -1596,6 +1710,7 @@ export const HUD_HTML = String.raw `<!doctype html>
1596
1710
  whyOpen = false;
1597
1711
  hud.classList.remove("show-why");
1598
1712
  nudge.style.display = "none";
1713
+ renderBillingStatus();
1599
1714
  if (renewBtn) renewBtn.style.display = "none";
1600
1715
  clearRenewResult();
1601
1716
  pathEl.textContent = "";
@@ -1617,6 +1732,27 @@ export const HUD_HTML = String.raw `<!doctype html>
1617
1732
  if (mrepoLine) mrepoLine.style.display = repo ? "flex" : "none";
1618
1733
  }
1619
1734
 
1735
+ function renderBillingStatus() {
1736
+ if (!billingBanner) return;
1737
+ const state = billingStatus && billingStatus.state;
1738
+ const blocked = state === "plan_required" || state === "quota_exceeded" || state === "billing_attention";
1739
+ billingBanner.style.display = blocked ? "block" : "none";
1740
+ billingBanner.classList.toggle("limit", state === "quota_exceeded");
1741
+ if (!blocked) return;
1742
+ const title = state === "quota_exceeded"
1743
+ ? "Recall limit reached"
1744
+ : state === "plan_required"
1745
+ ? "Recall paused · Free plan"
1746
+ : billingStatus.message || "Billing needs attention";
1747
+ const detail = state === "quota_exceeded"
1748
+ ? "You used all Pro searches this month. Upgrade or wait for reset."
1749
+ : state === "plan_required"
1750
+ ? "Search memory requires Pro or Power. Saving still works."
1751
+ : billingStatus.detail || "Open pricing to restore memory search.";
1752
+ if (billingTitle) billingTitle.textContent = title;
1753
+ if (billingCopy) billingCopy.textContent = detail;
1754
+ }
1755
+
1620
1756
  function renderWhy(score, oldPct, repeatCount, sat, health, color) {
1621
1757
  const rows = [];
1622
1758
  if (sat > 0) rows.push(["context", score.modelContextWindow ? sat + "% full · " + fmt(score.ctTokens) : fmt(score.ctTokens) + " tokens"]);
package/dist/index.js CHANGED
@@ -13,8 +13,10 @@ import { createHash, randomUUID } from "node:crypto";
13
13
  import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
14
14
  import { runCli } from "./setup.js";
15
15
  import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
16
+ import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
16
17
  import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
17
18
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
19
+ const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing";
18
20
  /** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
19
21
  class NoTokenError extends Error {
20
22
  }
@@ -80,6 +82,59 @@ function describeError(error) {
80
82
  const fallback = stringifyErrorValue(error);
81
83
  return fallback || "Unknown error";
82
84
  }
85
+ function appendPricingSource(url, source) {
86
+ try {
87
+ const parsed = new URL(url);
88
+ if (!parsed.searchParams.has("source")) {
89
+ parsed.searchParams.set("source", source);
90
+ }
91
+ return parsed.toString();
92
+ }
93
+ catch {
94
+ return url;
95
+ }
96
+ }
97
+ function formatUpgradeRequiredResult(error) {
98
+ if (!axios.isAxiosError(error))
99
+ return null;
100
+ const status = error.response?.status;
101
+ if (status !== 402 && status !== 403 && status !== 429)
102
+ return null;
103
+ const data = error.response?.data;
104
+ if (!isRecord(data))
105
+ return null;
106
+ const upgradeUrl = readString(data, "upgradeUrl")
107
+ ?? readString(data, "pricingUrl")
108
+ ?? readString(data, "url")
109
+ ?? ECHO_PRICING_URL;
110
+ const message = readString(data, "message")
111
+ ?? "Memory search and recall require an Echo Pro or Power plan.";
112
+ const code = readString(data, "error") ?? readString(data, "code");
113
+ const plan = readString(data, "plan");
114
+ const pricingUrl = appendPricingSource(upgradeUrl, "mcp_search");
115
+ const normalizedCode = String(code || "").toUpperCase();
116
+ const kind = status === 429 || normalizedCode.includes("QUOTA") || normalizedCode.includes("LIMIT")
117
+ ? "quota_exceeded"
118
+ : status === 402 || normalizedCode.includes("REQUIRES_PLAN")
119
+ ? "plan_required"
120
+ : "billing_attention";
121
+ writeBillingAlert({
122
+ kind,
123
+ code,
124
+ message,
125
+ plan,
126
+ pricingUrl,
127
+ });
128
+ return [
129
+ "ACTION REQUIRED: tell the user to start a 7-day Pro trial or choose a subscription at this exact URL:",
130
+ pricingUrl,
131
+ "When replying, include the exact URL above. Do not reply only with \"connect\" or \"upgrade\".",
132
+ "",
133
+ message,
134
+ "",
135
+ code ? `Reason: ${code}.` : "",
136
+ ].filter(Boolean).join("\n");
137
+ }
83
138
  /** Map a thrown error to the telemetry error_kind taxonomy. */
84
139
  function classifyError(error) {
85
140
  if (error instanceof NoTokenError)
@@ -108,7 +163,7 @@ function readNumber(record, key) {
108
163
  function errorCodeFrom(value) {
109
164
  if (!isRecord(value))
110
165
  return undefined;
111
- return readString(value, "code") || readString(value, "errorCode");
166
+ return readString(value, "error") || readString(value, "code") || readString(value, "errorCode");
112
167
  }
113
168
  function compactOneLine(value, maxLength) {
114
169
  if (!value)
@@ -1150,6 +1205,12 @@ class EchoMemMCPServer {
1150
1205
  if (error instanceof McpError) {
1151
1206
  throw error;
1152
1207
  }
1208
+ const upgradeRequired = formatUpgradeRequiredResult(error);
1209
+ if (upgradeRequired) {
1210
+ return {
1211
+ content: [{ type: "text", text: upgradeRequired }],
1212
+ };
1213
+ }
1153
1214
  return {
1154
1215
  content: [{ type: "text", text: `Error: ${describeError(error)}` }],
1155
1216
  isError: true,
@@ -1206,6 +1267,7 @@ class EchoMemMCPServer {
1206
1267
  rec.http_status = trace.httpStatus;
1207
1268
  rec.error_code = trace.errorCode;
1208
1269
  });
1270
+ clearBillingAlert();
1209
1271
  if (rec) {
1210
1272
  // "Which memories were used": topic keys + delivered context size, for the comparison dataset.
1211
1273
  const mems = Array.isArray(result?.memories) ? result.memories : [];
@@ -457,18 +457,24 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
457
457
  function recallGateUnlocked() {
458
458
  return recallGateSkipped || paidRecallPlan(billingStatus && billingStatus.plan);
459
459
  }
460
+ function recallTrialAvailable() {
461
+ return !billingStatus || billingStatus.trialAvailable !== false;
462
+ }
460
463
  function recallGateBlock() {
461
464
  var unlocked = recallGateUnlocked();
465
+ var trialAvailable = recallTrialAvailable();
462
466
  return '<div class="recallGateBlock">' +
463
467
  '<section class="recallGate' + (unlocked ? " is-hidden" : "") + '" id="recallGate">' +
464
- '<p class="recallEyebrow">Memory saved. Recall is next.</p>' +
465
- '<h3>Choose your 14-day trial plan for Codex and Claude recall.</h3>' +
466
- '<p>Saving conversations and extracting memory stay included. Compare Pro and Power, then choose the plan that should unlock <code>search_memories</code>, project recall, and the prompt below.</p>' +
468
+ '<p class="recallEyebrow"><i></i>Premium recall</p>' +
469
+ '<h3 id="recallGateTitle">' + (trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?") + '</h3>' +
470
+ '<p id="recallGateBody">' + (trialAvailable ? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way." : "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.") + '</p>' +
471
+ '<div class="recallTrial' + (trialAvailable ? "" : " is-hidden") + '" id="recallTrialBadge" aria-label="14 days free"><strong>14</strong><span>days free<br>then choose what fits</span></div>' +
472
+ '<p class="recallUsedNote' + (trialAvailable ? " is-hidden" : "") + '" id="recallUsedNote">Your trial was already used. You can still unlock recall by choosing Pro or Power.</p>' +
467
473
  '<div class="recallGateActions">' +
468
- '<button type="button" class="primary" id="recallStartTrial">Compare plans</button>' +
469
- '<button type="button" class="secondary" id="recallSkip">Skip for now</button>' +
474
+ '<button type="button" class="primary" id="recallStartTrial">' + (trialAvailable ? "Compare Pro and Power" : "See pricing") + '</button>' +
475
+ '<button type="button" class="textButton" id="recallSkip">Skip recall for now</button>' +
470
476
  '</div>' +
471
- '<p class="recallFine">Card required for trial. Cancel anytime.</p>' +
477
+ '<p class="recallFine" id="recallFine">' + (trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.") + '</p>' +
472
478
  '<p class="launchStatus" id="recallGateStatus"></p>' +
473
479
  '</section>' +
474
480
  '<div id="recallTryWrap" class="recallTryWrap' + (unlocked ? "" : " is-hidden") + '">' +
@@ -489,6 +495,21 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
489
495
  if (gate) gate.classList.toggle("is-hidden", unlocked);
490
496
  if (wrap) wrap.classList.toggle("is-hidden", !unlocked);
491
497
  if (skipNote) skipNote.classList.toggle("is-hidden", !recallGateSkipped);
498
+ var trialAvailable = recallTrialAvailable();
499
+ var badge = document.getElementById("recallTrialBadge");
500
+ if (badge) badge.classList.toggle("is-hidden", !trialAvailable);
501
+ var usedNote = document.getElementById("recallUsedNote");
502
+ if (usedNote) usedNote.classList.toggle("is-hidden", trialAvailable);
503
+ var title = document.getElementById("recallGateTitle");
504
+ if (title) title.textContent = trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?";
505
+ var body = document.getElementById("recallGateBody");
506
+ if (body) body.textContent = trialAvailable
507
+ ? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way."
508
+ : "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.";
509
+ var cta = document.getElementById("recallStartTrial");
510
+ if (cta) cta.textContent = trialAvailable ? "Compare Pro and Power" : "See pricing";
511
+ var fine = document.getElementById("recallFine");
512
+ if (fine) fine.textContent = trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.";
492
513
  }
493
514
  async function refreshBillingStatus() {
494
515
  try {
@@ -683,62 +683,118 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
683
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); }
684
684
  .exOverdue strong { display: block; margin-bottom: 4px; color: var(--echo-ink-primary); }
685
685
  .is-hidden { display: none !important; }
686
- .recallGateBlock { width: min(760px, 100%); margin: 2px auto 0; }
686
+ .recallGateBlock { width: min(680px, 100%); margin: 2px auto 0; }
687
687
  .recallGate {
688
688
  width: 100%;
689
- border: 2px solid var(--echo-ink-primary);
690
- border-radius: 8px;
691
- background: var(--echo-paper-white);
692
- padding: clamp(18px, 3vw, 26px);
693
- box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45);
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);
694
698
  text-align: left;
695
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
+ }
696
707
  .recallEyebrow {
697
- margin: 0 0 10px;
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;
698
716
  font-family: var(--echo-font-mono);
699
717
  font-size: 11px;
700
- font-weight: 800;
701
- letter-spacing: 0.08em;
718
+ font-weight: 900;
719
+ letter-spacing: 0.06em;
702
720
  text-transform: uppercase;
703
- color: var(--echo-ink-primary);
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);
704
729
  }
705
730
  .recallGate h3 {
706
731
  margin: 0;
707
- max-width: 680px;
732
+ max-width: 520px;
708
733
  font-family: var(--echo-font-brand);
709
- font-size: clamp(30px, 3.8vw, 48px);
710
- line-height: 0.98;
734
+ font-size: clamp(34px, 4.6vw, 48px);
735
+ line-height: 1.02;
711
736
  font-weight: 900;
712
- color: var(--echo-ink-primary);
737
+ color: var(--echo-ink-text);
713
738
  letter-spacing: 0;
714
739
  }
715
740
  .recallGate p {
716
- max-width: 620px;
741
+ max-width: 560px;
717
742
  margin: 14px 0 0;
718
- font-size: 15px;
743
+ font-size: 16px;
719
744
  line-height: 1.55;
720
745
  color: var(--echo-ink-mute);
721
746
  }
722
- .recallGate code,
723
- .recallSkipNote code {
724
- font-family: var(--echo-font-mono);
725
- font-size: 0.92em;
726
- background: var(--echo-paper-soft);
727
- border: 1px solid var(--echo-line-soft);
728
- border-radius: 5px;
729
- padding: 1px 5px;
730
- color: var(--echo-ink-primary);
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;
731
778
  }
732
779
  .recallGateActions {
733
780
  display: flex;
734
781
  flex-wrap: wrap;
735
- gap: 10px;
782
+ gap: 14px;
736
783
  align-items: center;
737
- margin-top: 22px;
784
+ margin-top: 24px;
738
785
  }
739
786
  .recallGateActions button {
740
- min-height: 48px;
741
- padding: 0 18px;
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;
742
798
  }
743
799
  .recallGate .recallFine {
744
800
  margin-top: 10px;
@@ -746,6 +802,19 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
746
802
  font-size: 11px;
747
803
  color: var(--echo-ink-faint);
748
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
+ }
749
818
  .recallTryWrap { width: 100%; }
750
819
  .recallSkipNote {
751
820
  margin: 0 0 12px;
package/dist/setup.js CHANGED
@@ -1257,20 +1257,24 @@ export function startCallbackServer(opts = {}) {
1257
1257
  const token = new KeyStore().getToken();
1258
1258
  const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
1259
1259
  if (!token) {
1260
- json(res, 200, { plan: "free", paid: false, pricingUrl });
1260
+ json(res, 200, { plan: "free", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
1261
1261
  return;
1262
1262
  }
1263
1263
  try {
1264
1264
  const response = await authedAxios(token).get("/api/extension/account/bootstrap", { timeout: 6000 });
1265
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;
1266
1268
  json(res, 200, {
1267
1269
  plan,
1268
1270
  paid: ["pro", "power", "team", "enterprise"].includes(plan),
1271
+ trialAvailable,
1272
+ trialUsed,
1269
1273
  pricingUrl,
1270
1274
  });
1271
1275
  }
1272
1276
  catch {
1273
- json(res, 200, { plan: "unknown", paid: false, pricingUrl });
1277
+ json(res, 200, { plan: "unknown", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
1274
1278
  }
1275
1279
  return;
1276
1280
  }
@@ -109,6 +109,7 @@ export function listToolSpecs(opts = {}) {
109
109
  const map = opts.map?.trim();
110
110
  const updateNotice = opts.updateNotice?.trim();
111
111
  const paidRecallNote = "Requires Echo Pro or Power; saving conversations and memory extraction remain included.";
112
+ const searchBillingReplyInstruction = "If search returns an ACTION REQUIRED subscription message, tell the user to start their trial or subscription and include the exact URL from that result verbatim. Do not respond only with \"connect\" or \"upgrade\".";
112
113
  const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
113
114
  const mapSection = map
114
115
  ? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
@@ -116,7 +117,7 @@ export function listToolSpecs(opts = {}) {
116
117
  return [
117
118
  {
118
119
  name: canonicalToolNames.search,
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}`),
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. ${paidRecallNote} ${searchBillingReplyInstruction}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
120
121
  inputSchema: {
121
122
  type: "object",
122
123
  properties: {
@@ -139,7 +140,7 @@ export function listToolSpecs(opts = {}) {
139
140
  },
140
141
  {
141
142
  name: "search_memories_by_description_semantic",
142
- description: `Legacy alias for search_memories. ${paidRecallNote}`,
143
+ description: `Legacy alias for search_memories. ${paidRecallNote} ${searchBillingReplyInstruction}`,
143
144
  inputSchema: {
144
145
  type: "object",
145
146
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.15",
3
+ "version": "1.4.17",
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",