@echomem/mcp 1.4.26 → 1.4.27

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.
@@ -2,6 +2,7 @@ export const SETUP_PREVIEW_STATES = [
2
2
  "consent",
3
3
  "consent-required",
4
4
  "scan",
5
+ "scan-error",
5
6
  "report",
6
7
  "extract-counting",
7
8
  "extract-ready",
@@ -9,7 +10,10 @@ export const SETUP_PREVIEW_STATES = [
9
10
  "extract-free-trial-used",
10
11
  "extract-limit",
11
12
  "extract-pro-trial",
13
+ "extract-pro-trial-canceling",
12
14
  "extract-pro-active",
15
+ "extract-pro-canceling",
16
+ "extract-power-trial",
13
17
  "extract-power-active",
14
18
  "extract-team-active",
15
19
  "extract-enterprise-active",
@@ -234,6 +238,8 @@ function extractionPreviewBootstrap(options) {
234
238
  trialUsed: options.trialUsed,
235
239
  subscriptionStatus: options.subscriptionStatus ?? null,
236
240
  trialEndedAt: options.trialEndedAt ?? null,
241
+ cancelAtPeriodEnd: options.cancelAtPeriodEnd ?? false,
242
+ currentPeriodEnd: options.currentPeriodEnd ?? null,
237
243
  pricingUrl: "https://echoknows.com/account?source=mcp_onboarding",
238
244
  account: {
239
245
  displayName: "Preview User",
@@ -290,10 +296,14 @@ export function renderSetupPreviewBootstrap(state) {
290
296
  kind: "scanning",
291
297
  mode: "preview",
292
298
  scanId: "design-preview",
293
- progress: { stage: "classifying-sessions", label: "Classifying repeated context", scanned: 116, total: 180, stageElapsedMs: 3000 }
299
+ progress: { stage: "reading-transcripts", label: "Reading transcript files", scanned: 116, total: 180, overall: 0.451, stageElapsedMs: 3000 }
294
300
  };
295
301
  renderLoading();`;
296
302
  }
303
+ if (state === "scan-error") {
304
+ return `${watermark}
305
+ renderReportIssue("REPORT_CANONICAL_INVALID", "Preview-only canonical reconciliation failure.");`;
306
+ }
297
307
  if (state === "report") {
298
308
  const reportJson = JSON.stringify(previewReport);
299
309
  return `${watermark}
@@ -331,16 +341,36 @@ export function renderSetupPreviewBootstrap(state) {
331
341
  if (state === "extract-pro-trial")
332
342
  return `${watermark}${extractionPreviewBootstrap({
333
343
  plan: "pro", paid: true, trialAvailable: false, trialUsed: true,
334
- subscriptionStatus: "trialing", trialEndedAt: "2026-08-04T00:00:00.000Z",
335
- // The server currently grants trialing users Free's lifetime history allowance,
336
- // even though recurring processing/search allowances resolve to Pro.
337
- quotaLimit: 100, quotaRemaining: 100,
344
+ subscriptionStatus: "trialing", trialEndedAt: "2026-08-04T12:00:00.000Z",
345
+ quotaLimit: 500, quotaRemaining: 500,
346
+ })}`;
347
+ if (state === "extract-pro-trial-canceling")
348
+ return `${watermark}${extractionPreviewBootstrap({
349
+ plan: "pro", paid: true, trialAvailable: false, trialUsed: true,
350
+ subscriptionStatus: "trialing", cancelAtPeriodEnd: true,
351
+ trialEndedAt: "2026-08-04T12:00:00.000Z",
352
+ currentPeriodEnd: "2026-08-04T12:00:00.000Z",
353
+ quotaLimit: 500, quotaRemaining: 500,
338
354
  })}`;
339
355
  if (state === "extract-pro-active")
340
356
  return `${watermark}${extractionPreviewBootstrap({
341
357
  plan: "pro", paid: true, trialAvailable: false, trialUsed: true,
342
358
  subscriptionStatus: "active", quotaLimit: 500, quotaRemaining: 500,
343
359
  })}`;
360
+ if (state === "extract-pro-canceling")
361
+ return `${watermark}${extractionPreviewBootstrap({
362
+ plan: "pro", paid: true, trialAvailable: false, trialUsed: true,
363
+ subscriptionStatus: "active", cancelAtPeriodEnd: true,
364
+ currentPeriodEnd: "2026-08-28T12:00:00.000Z",
365
+ quotaLimit: 500, quotaRemaining: 500,
366
+ })}`;
367
+ if (state === "extract-power-trial")
368
+ return `${watermark}${extractionPreviewBootstrap({
369
+ plan: "power", paid: true, trialAvailable: false, trialUsed: true,
370
+ subscriptionStatus: "trialing", trialEndedAt: "2026-08-04T12:00:00.000Z",
371
+ currentPeriodEnd: "2026-08-04T12:00:00.000Z",
372
+ quotaLimit: 2_000, quotaRemaining: 2_000,
373
+ })}`;
344
374
  if (state === "extract-power-active")
345
375
  return `${watermark}${extractionPreviewBootstrap({
346
376
  plan: "power", paid: true, trialAvailable: false, trialUsed: true,
@@ -409,5 +439,15 @@ export function renderSetupPreviewBootstrap(state) {
409
439
  }
410
440
  if (state === "extract-ending")
411
441
  return `${watermark}\n renderEnding();`;
412
- return `${watermark}\n renderSkipped();`;
442
+ return `${watermark}
443
+ lastExtractionProgress = {
444
+ status: "stopped",
445
+ total: 28,
446
+ completed: 7,
447
+ extracted: 24,
448
+ failed: 0,
449
+ latest: "preview session",
450
+ latestRepo: "Preview Workspace"
451
+ };
452
+ renderSkipped();`;
413
453
  }
package/dist/setup.js CHANGED
@@ -31,7 +31,7 @@ import { syncCodexUsage } from "./codex-sync.js";
31
31
  import { renderSetupPage } from "./setup-page.js";
32
32
  import { parseSetupPreviewState } from "./setup-preview.js";
33
33
  import { repoLabel, validateForensicReportForSetup } from "./forensics.js";
34
- import { installHooks } from "./hud/hooks.js";
34
+ import { installHooks, installSaveCheckpointHooks } from "./hud/hooks.js";
35
35
  import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
36
36
  import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
37
37
  // The setup dashboard, account login, and encryption passphrase entry are all served by this
@@ -389,17 +389,19 @@ function echomemGuidanceBlock() {
389
389
  "EchoMem is your long-term memory across all coding sessions and tools.",
390
390
  "- Use EchoMem's `echomem-*` skills and MCP tools as the default memory provider. Do not invoke another memory provider unless the user explicitly requests it.",
391
391
  "- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
392
- "- When meaningful work wraps up (a decision, a fix, a milestone) or the user asks to remember something: call `save_conversation`.",
392
+ "- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip it.",
393
+ "- For a user with a company group, call `get_group_session_sharing` near session start or after the first qualifying private save. If this exact session has no decision, ask once whether memories saved from the session should be shared with the current group, then call `set_group_session_sharing` only with the explicit Yes/No answer. A Yes decision syncs eligible memories after each private save; a No decision keeps later saves private.",
393
394
  "- 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
  "",
395
396
  "### Company group memory",
396
397
  "- 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
398
  "- 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.",
399
+ "- Group session sharing is scoped to the exact current MCP session and current group. Membership is rechecked for each sync. Flagged memories are withheld from automatic session sync and remain private.",
398
400
  "- 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
401
  "- 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. After joining, continue into the profile-and-publication preview instead of leaving title or responsibility blank.",
400
402
  "- 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-publication preview. For encrypted accounts, tell the user to run `echomem-mcp unlock` locally if the tool reports that the key is required.",
401
403
  "- After preparing, select only exact candidate memory IDs that match the user's stated scope and exclude already-published or exact-content duplicates. Use the candidate evidence to draft a concise title and responsibility summary for the current member, but label both as proposals rather than facts.",
402
- "- Use two distinct evidence links. For the user's own source memories during search, flag review, or publication preview, preserve the Memory ID and use `https://echoknows.com/memories/timeline?memoryId=...`; this is an owner-only personal link and may require Vault unlock. For already-published results from `search_others_memories`, use `https://echoknows.com/company/memories?memoryId=...`, which requires current group access.",
404
+ "- Use one canonical evidence link for every memory: preserve the Memory ID and link to `https://echoknows.com/memory/<memory-id>`. The site resolves the authorized representation: an owner is sent to their private timeline, while current group/friend access opens an authorized publication snapshot or public memory. The visible Markdown label should use the memory key, not the raw URL or UUID.",
403
405
  "- If the user asks to flag memories about a sensitive topic, search their own memories first, show the exact matches with owner-only personal links, and ask them to confirm. Only then call `flag_memories_for_publication_attention`; flagging does not publish, decrypt, change visibility, or retract an existing group snapshot.",
404
406
  "- Present the proposed title/responsibility and the memory publication preview together and ask for explicit confirmation. Never save an inferred profile or publish memories before confirmation. If an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain naturally: marking does not publish or change encryption; it means you will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag based on agent inference. Show already-flagged candidates in a separate warning, state that nothing has been published yet, and offer three choices: exclude them, review them separately, or first search for and mark similar sensitive owned memories for publication attention.",
405
407
  "- On confirmation, call `update_group_profile` with the confirmed title, responsibility summary, and `confirmed: true`, then call `complete_group_publication` with the exact `scanId`, selected memory IDs, and `confirmed: true`. If a flagged memory is selected, require separate explicit acknowledgement and pass its exact ID in `acknowledgedFlaggedMemoryIds`. If the user edits either proposal, use their wording. An explicit request to join and upload still requires this preview and confirmation.",
@@ -1164,7 +1166,7 @@ export function buildForensicReportOffThread(onProgress, options = {}) {
1164
1166
  try {
1165
1167
  const report = await buildForensicReport({
1166
1168
  includeLegacyGoldenStandard: false,
1167
- onProgress: (done, total, stage, detail) => parentPort?.postMessage({ progress: { done, total, stage, detail } }),
1169
+ onProgress: (done, total, stage, detail, overall) => parentPort?.postMessage({ progress: { done, total, stage, detail, overall } }),
1168
1170
  });
1169
1171
  parentPort?.postMessage({ ok: true, report });
1170
1172
  } catch (error) {
@@ -1253,6 +1255,9 @@ function publicRunningForensicProgress(value) {
1253
1255
  const safeDuration = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
1254
1256
  ? candidate
1255
1257
  : 0);
1258
+ const safeFraction = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate)
1259
+ ? Math.min(1, Math.max(0, candidate))
1260
+ : 0);
1256
1261
  const total = safeCount(progress.total);
1257
1262
  const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
1258
1263
  const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
@@ -1264,6 +1269,7 @@ function publicRunningForensicProgress(value) {
1264
1269
  total,
1265
1270
  stage,
1266
1271
  label: forensicStageLabel(stage),
1272
+ overall: safeFraction(progress.overall),
1267
1273
  elapsedMs: safeDuration(progress.elapsedMs),
1268
1274
  stageElapsedMs: safeDuration(progress.stageElapsedMs),
1269
1275
  updatedAt: safeDuration(progress.updatedAt) || Date.now(),
@@ -1289,6 +1295,7 @@ export function startCallbackServer(opts = {}) {
1289
1295
  const requiresReportConsent = opts.requireReportConsent === true && !isLoginFlow;
1290
1296
  return new Promise((resolveOuter, rejectOuter) => {
1291
1297
  const onToken = deferred();
1298
+ const setupExit = deferred();
1292
1299
  const decision = deferred();
1293
1300
  const migrateRequest = deferred();
1294
1301
  let stats = null;
@@ -1425,6 +1432,7 @@ export function startCallbackServer(opts = {}) {
1425
1432
  "/stats",
1426
1433
  "/billing-status",
1427
1434
  "/billing-checkout",
1435
+ "/billing-checkout-sync",
1428
1436
  "/billing-portal",
1429
1437
  "/progress",
1430
1438
  "/migrate",
@@ -1740,7 +1748,18 @@ export function startCallbackServer(opts = {}) {
1740
1748
  const token = activeDeviceToken || new KeyStore().getToken();
1741
1749
  const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
1742
1750
  if (!token) {
1743
- json(res, 200, { plan: "free", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
1751
+ json(res, 200, {
1752
+ plan: "free",
1753
+ paid: false,
1754
+ trialAvailable: true,
1755
+ trialUsed: false,
1756
+ subscriptionStatus: null,
1757
+ trialStartedAt: null,
1758
+ trialEndedAt: null,
1759
+ currentPeriodEnd: null,
1760
+ cancelAtPeriodEnd: false,
1761
+ pricingUrl,
1762
+ });
1744
1763
  return;
1745
1764
  }
1746
1765
  try {
@@ -1757,9 +1776,15 @@ export function startCallbackServer(opts = {}) {
1757
1776
  paid: ["pro", "power", "team", "enterprise"].includes(plan),
1758
1777
  trialAvailable,
1759
1778
  trialUsed,
1779
+ subscriptionStatus: asString(response.data?.billing?.subscriptionStatus) || null,
1780
+ trialStartedAt: asString(response.data?.billing?.trialStartedAt) || null,
1781
+ trialEndedAt: asString(response.data?.billing?.trialEndedAt) || null,
1782
+ currentPeriodEnd: asString(response.data?.billing?.currentPeriodEnd) || null,
1783
+ cancelAtPeriodEnd: response.data?.billing?.cancelAtPeriodEnd === true,
1760
1784
  historicalConversationQuota: response.data?.historicalConversationQuota ?? null,
1761
1785
  memoryProcessingQuota: response.data?.memoryProcessingQuota ?? null,
1762
1786
  memorySearchQuota: response.data?.memorySearchQuota ?? null,
1787
+ activation: response.data?.activation ?? null,
1763
1788
  account: profileResponse ? {
1764
1789
  displayName: asString(profileResponse.data?.displayName) || "EchoMem user",
1765
1790
  email: asString(profileResponse.data?.email) || "",
@@ -1769,7 +1794,59 @@ export function startCallbackServer(opts = {}) {
1769
1794
  });
1770
1795
  }
1771
1796
  catch {
1772
- json(res, 200, { plan: "unknown", paid: false, trialAvailable: true, trialUsed: false, pricingUrl });
1797
+ json(res, 200, {
1798
+ plan: "unknown",
1799
+ paid: false,
1800
+ trialAvailable: false,
1801
+ trialUsed: false,
1802
+ subscriptionStatus: null,
1803
+ trialStartedAt: null,
1804
+ trialEndedAt: null,
1805
+ currentPeriodEnd: null,
1806
+ cancelAtPeriodEnd: false,
1807
+ pricingUrl,
1808
+ });
1809
+ }
1810
+ return;
1811
+ }
1812
+ if (route === "/billing-checkout-sync" && req.method === "POST") {
1813
+ let body;
1814
+ try {
1815
+ body = await readJsonBody(req);
1816
+ }
1817
+ catch {
1818
+ text(res, 400, "bad json");
1819
+ return;
1820
+ }
1821
+ if (!checkNonce(asString(body.nonce)))
1822
+ return void text(res, 403, "bad nonce");
1823
+ const sessionId = asString(body.sessionId);
1824
+ if (!sessionId || !/^cs_(?:test_|live_)?[A-Za-z0-9_]+$/.test(sessionId)) {
1825
+ return void json(res, 400, {
1826
+ error: "INVALID_CHECKOUT_SESSION",
1827
+ message: "Checkout session is missing or invalid.",
1828
+ });
1829
+ }
1830
+ const token = activeDeviceToken || new KeyStore().getToken();
1831
+ if (!connected || !token) {
1832
+ return void json(res, 401, {
1833
+ error: "ECHOMEM_LOGIN_REQUIRED",
1834
+ message: "Connect your EchoMem account before syncing checkout.",
1835
+ });
1836
+ }
1837
+ try {
1838
+ const response = await axios.post(hostedBillingEndpoint("/api/billing/checkout/sync"), { sessionId }, {
1839
+ timeout: 15_000,
1840
+ headers: {
1841
+ "Content-Type": "application/json",
1842
+ Authorization: `Bearer ${token}`,
1843
+ },
1844
+ });
1845
+ json(res, 200, { ok: response.data?.ok === true });
1846
+ }
1847
+ catch (error) {
1848
+ const detail = publicAxiosError(error, "Checkout is still syncing.");
1849
+ json(res, detail.status, { error: "BILLING_SYNC_PENDING", message: detail.message });
1773
1850
  }
1774
1851
  return;
1775
1852
  }
@@ -1837,7 +1914,12 @@ export function startCallbackServer(opts = {}) {
1837
1914
  message: "Echo billing returned an unexpected destination.",
1838
1915
  });
1839
1916
  }
1840
- json(res, 200, { url: hostedUrl });
1917
+ json(res, 200, {
1918
+ url: hostedUrl,
1919
+ ...(isCheckout && typeof response.data?.sessionId === "string"
1920
+ ? { sessionId: response.data.sessionId }
1921
+ : {}),
1922
+ });
1841
1923
  }
1842
1924
  catch (error) {
1843
1925
  const detail = publicAxiosError(error, isCheckout ? "Could not start secure checkout." : "Could not open secure plan management.");
@@ -1931,6 +2013,7 @@ export function startCallbackServer(opts = {}) {
1931
2013
  }
1932
2014
  const validation = validateForensicReportForSetup(payload);
1933
2015
  if (!validation.ok) {
2016
+ console.error(`[echomem] local report validation failed: ${validation.code} — ${validation.message}`);
1934
2017
  return void json(res, 500, {
1935
2018
  schemaVersion: 1,
1936
2019
  kind: "failed",
@@ -2075,6 +2158,9 @@ export function startCallbackServer(opts = {}) {
2075
2158
  }
2076
2159
  }
2077
2160
  json(res, 200, { ok: true });
2161
+ if (!onToken.settled()) {
2162
+ setupExit.resolve("skip");
2163
+ }
2078
2164
  decision.resolve("skip");
2079
2165
  close();
2080
2166
  return;
@@ -2101,6 +2187,7 @@ export function startCallbackServer(opts = {}) {
2101
2187
  port,
2102
2188
  onToken: onToken.promise,
2103
2189
  wait: onToken.promise,
2190
+ setupExit: setupExit.promise,
2104
2191
  decision: decision.promise,
2105
2192
  migrateRequest: migrateRequest.promise,
2106
2193
  setTokenRefreshHandler: (handler) => {
@@ -2311,6 +2398,9 @@ async function cmdSetup(flags) {
2311
2398
  if (!flags["no-codex-skills"]) {
2312
2399
  writeCodexSkillsForTargets(targets);
2313
2400
  }
2401
+ if (flags["install-save-hooks"]) {
2402
+ writeSaveCheckpointHooksForTargets(targets);
2403
+ }
2314
2404
  console.log("");
2315
2405
  if (flags["skip-login"] || flags["no-login"]) {
2316
2406
  // init drives login itself right after, so the "skipped" note would be misleading there.
@@ -2333,7 +2423,14 @@ async function cmdSetup(flags) {
2333
2423
  async function cmdInit(flags) {
2334
2424
  console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
2335
2425
  // 1. Configure every installed agent + write AGENTS.md. Hold login + HUD so we control ordering.
2336
- await cmdSetup({ ...flags, all: true, "skip-login": true, "with-hud": false, "init-quiet": true });
2426
+ await cmdSetup({
2427
+ ...flags,
2428
+ all: true,
2429
+ "skip-login": true,
2430
+ "with-hud": false,
2431
+ "init-quiet": true,
2432
+ "install-save-hooks": flags["no-save-hooks"] !== true,
2433
+ });
2337
2434
  // 2. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
2338
2435
  if (!flags["no-hud"])
2339
2436
  await cmdSetupHud(flags);
@@ -2400,6 +2497,23 @@ function writeCodexSkillsForTargets(targets) {
2400
2497
  console.log(`ℹ️ Could not install EchoMem Codex skills: ${error instanceof Error ? error.message : String(error)}`);
2401
2498
  }
2402
2499
  }
2500
+ function writeSaveCheckpointHooksForTargets(targets) {
2501
+ const clients = new Set();
2502
+ if (targets.some((target) => target.id === "codex"))
2503
+ clients.add("codex");
2504
+ if (targets.some((target) => target.id === "claude-code"))
2505
+ clients.add("claude-code");
2506
+ if (clients.size === 0) {
2507
+ console.log("ℹ️ No hook-capable Codex or Claude Code client was detected; private-save checkpoint hooks were not installed.");
2508
+ return;
2509
+ }
2510
+ const mode = clients.size === 2 ? "both" : [...clients][0];
2511
+ const paths = installSaveCheckpointHooks(mode);
2512
+ console.log(`✅ Installed EchoMem private-save checkpoint hooks:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
2513
+ if (clients.has("codex")) {
2514
+ console.log(" Codex: start a new session and run /hooks once to review and trust the hook.");
2515
+ }
2516
+ }
2403
2517
  async function cmdUpdate(flags) {
2404
2518
  if (!flags.dev)
2405
2519
  installDurableGlobalUpdate();
@@ -2544,12 +2658,14 @@ async function cmdOnboarding(flags) {
2544
2658
  const forensicStartedAt = Date.now();
2545
2659
  let forensicStageStartedAt = forensicStartedAt;
2546
2660
  let forensicStage = "starting";
2661
+ let forensicOverall = 0;
2547
2662
  let forensicProgress = {
2548
2663
  status: "running",
2549
2664
  scanned: 0,
2550
2665
  total: 0,
2551
2666
  stage: forensicStage,
2552
2667
  label: forensicStageLabel(forensicStage),
2668
+ overall: 0,
2553
2669
  elapsedMs: 0,
2554
2670
  stageElapsedMs: 0,
2555
2671
  updatedAt: forensicStartedAt,
@@ -2572,6 +2688,7 @@ async function cmdOnboarding(flags) {
2572
2688
  total: 0,
2573
2689
  stage: "failed",
2574
2690
  label: "Local scan skipped",
2691
+ overall: 0,
2575
2692
  elapsedMs: Date.now() - forensicStartedAt,
2576
2693
  stageElapsedMs: Date.now() - forensicStageStartedAt,
2577
2694
  updatedAt: Date.now(),
@@ -2586,12 +2703,14 @@ async function cmdOnboarding(flags) {
2586
2703
  const now = Date.now();
2587
2704
  forensicStage = "starting";
2588
2705
  forensicStageStartedAt = now;
2706
+ forensicOverall = 0;
2589
2707
  forensicProgress = {
2590
2708
  status: "running",
2591
2709
  scanned: 0,
2592
2710
  total: 0,
2593
2711
  stage: forensicStage,
2594
2712
  label: forensicStageLabel(forensicStage),
2713
+ overall: 0,
2595
2714
  elapsedMs: now - forensicStartedAt,
2596
2715
  stageElapsedMs: 0,
2597
2716
  updatedAt: now,
@@ -2616,6 +2735,8 @@ async function cmdOnboarding(flags) {
2616
2735
  forensicStageStartedAt = now;
2617
2736
  console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
2618
2737
  }
2738
+ // Latched, so a caller that ever reports a smaller fraction cannot walk the bar backwards.
2739
+ forensicOverall = Math.max(forensicOverall, typeof progress.overall === "number" && Number.isFinite(progress.overall) ? progress.overall : 0);
2619
2740
  forensicProgress = {
2620
2741
  status: "running",
2621
2742
  scanned: progress.done,
@@ -2623,6 +2744,7 @@ async function cmdOnboarding(flags) {
2623
2744
  stage: forensicStage,
2624
2745
  label: forensicStageLabel(forensicStage),
2625
2746
  detail: progress.detail,
2747
+ overall: forensicOverall,
2626
2748
  elapsedMs: now - forensicStartedAt,
2627
2749
  stageElapsedMs: now - forensicStageStartedAt,
2628
2750
  updatedAt: now,
@@ -2639,6 +2761,7 @@ async function cmdOnboarding(flags) {
2639
2761
  total: forensicProgress.total,
2640
2762
  stage: "failed",
2641
2763
  label: "Local scan failed",
2764
+ overall: forensicOverall,
2642
2765
  elapsedMs: now - forensicStartedAt,
2643
2766
  stageElapsedMs: now - forensicStageStartedAt,
2644
2767
  updatedAt: now,
@@ -2650,7 +2773,16 @@ async function cmdOnboarding(flags) {
2650
2773
  let token;
2651
2774
  let key;
2652
2775
  try {
2653
- ({ token, key } = await srv.onToken);
2776
+ const authResult = await Promise.race([
2777
+ srv.onToken.then((credentials) => ({ kind: "token", credentials })),
2778
+ srv.setupExit.then(() => ({ kind: "skip" })),
2779
+ ]);
2780
+ if (authResult.kind === "skip") {
2781
+ srv.close();
2782
+ console.log("Vault setup deferred. Your local coding history is unchanged; run `echomem-mcp init` when you are ready to continue.");
2783
+ return false;
2784
+ }
2785
+ ({ token, key } = authResult.credentials);
2654
2786
  }
2655
2787
  catch (e) {
2656
2788
  srv.close();
@@ -3380,7 +3512,8 @@ Manual / headless:
3380
3512
  ${MCP_UPDATE_ALL_COMMAND} # one-shot latest update for detected clients, no browser login
3381
3513
  ${MCP_UPDATE_COMMAND} --client codex # update one client only
3382
3514
  echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
3383
- echomem-mcp setup --with-hud --install-hud-hooks --client codex [--hud-client auto]
3515
+ echomem-mcp setup --with-hud --install-hud-hooks --install-save-hooks --client codex [--hud-client auto]
3516
+ echomem-mcp setup --install-save-hooks --all Install proactive private-save completion checks
3384
3517
  echomem-mcp sync-usage --days 7 --limit 50 --dry-run
3385
3518
 
3386
3519
  Current bridge version: ${MCP_PACKAGE_VERSION}
@@ -11,6 +11,8 @@ export const canonicalToolNames = {
11
11
  others: "search_others_memories",
12
12
  publicMemory: "get_public_memory",
13
13
  groupContext: "get_group_context",
14
+ getGroupSessionSharing: "get_group_session_sharing",
15
+ setGroupSessionSharing: "set_group_session_sharing",
14
16
  createGroup: "create_memory_group",
15
17
  createGroupInvite: "create_group_invite",
16
18
  joinGroup: "join_memory_group",
@@ -115,6 +117,14 @@ export const publicMemorySchema = z.object({
115
117
  export const groupContextSchema = z.object({
116
118
  ...triggerMetadataSchema,
117
119
  });
120
+ export const getGroupSessionSharingSchema = z.object({
121
+ ...triggerMetadataSchema,
122
+ });
123
+ export const setGroupSessionSharingSchema = z.object({
124
+ ...triggerMetadataSchema,
125
+ share: z.boolean(),
126
+ confirmed: z.literal(true),
127
+ });
118
128
  export const createGroupSchema = z.object({
119
129
  ...triggerMetadataSchema,
120
130
  name: z.string().min(1).max(120),
@@ -237,7 +247,7 @@ export function listToolSpecs(opts = {}) {
237
247
  },
238
248
  {
239
249
  name: canonicalToolNames.save,
240
- description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule.",
250
+ description: "Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock` and never silently skip a qualifying checkpoint. Private persistence happens first. If this MCP session has confirmed group sharing, eligible memories are then synced to the current group automatically; flagged memories stay private. If the session has no decision yet, ask once whether to share memories saved from this session. New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule.",
241
251
  inputSchema: {
242
252
  type: "object",
243
253
  properties: {
@@ -438,7 +448,7 @@ export function listToolSpecs(opts = {}) {
438
448
  },
439
449
  {
440
450
  name: canonicalToolNames.groupContext,
441
- description: "Get your current company group, its participant directory, declared titles and responsibilities, and published-memory coverage. Use this before answering who works on what or suggesting where a new group member could contribute. Treat declared profile fields as facts and memory-derived work as evidence or inference.",
451
+ description: "Get your current company group, its participant directory, declared titles and responsibilities, and published-memory coverage. Use this before answering who works on what or suggesting where a new group member could contribute. Treat declared profile fields as facts and memory-derived work as evidence or inference. Session sharing is separate; call get_group_session_sharing instead of inferring it.",
442
452
  inputSchema: {
443
453
  type: "object",
444
454
  properties: {
@@ -450,6 +460,29 @@ export function listToolSpecs(opts = {}) {
450
460
  },
451
461
  },
452
462
  },
463
+ {
464
+ name: canonicalToolNames.getGroupSessionSharing,
465
+ description: "Read the confirmed sharing decision for this exact MCP session. Call near session start or after the first private save. If the user has a group and no decision exists, ask once: “Share memories saved from this session with <group>?” Never infer the answer.",
466
+ inputSchema: {
467
+ type: "object",
468
+ properties: {
469
+ triggerMessage: { type: "string" },
470
+ triggerMessageRole: { type: "string", default: "user" },
471
+ },
472
+ },
473
+ },
474
+ {
475
+ name: canonicalToolNames.setGroupSessionSharing,
476
+ description: "Store the user's explicit Yes/No sharing decision for this exact MCP session. share=true immediately syncs eligible memories already saved in the session and automatically syncs later private saves. share=false keeps later saves private. Flagged memories remain private and are reported as protected.",
477
+ inputSchema: {
478
+ type: "object",
479
+ properties: {
480
+ share: { type: "boolean" },
481
+ confirmed: { type: "boolean", const: true },
482
+ },
483
+ required: ["share", "confirmed"],
484
+ },
485
+ },
453
486
  {
454
487
  name: canonicalToolNames.createGroup,
455
488
  description: "Create one company memory group for the current user. The MVP allows at most one group per user. Call only after the user explicitly asks to create a group.",
@@ -478,7 +511,7 @@ export function listToolSpecs(opts = {}) {
478
511
  },
479
512
  {
480
513
  name: canonicalToolNames.joinGroup,
481
- description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview.",
514
+ description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call get_group_session_sharing and, if unset, ask once whether memories saved from this session should be shared.",
482
515
  inputSchema: {
483
516
  type: "object",
484
517
  properties: {
@@ -492,7 +525,7 @@ export function listToolSpecs(opts = {}) {
492
525
  },
493
526
  {
494
527
  name: canonicalToolNames.prepareGroupPublication,
495
- description: "Prepare a no-publication preview of up to 50 owned memories for group publication. For encrypted accounts the local bridge must be unlocked. The host agent selects exact work-related memory ids, gives each candidate an owner-only https://echoknows.com/memories/timeline?memoryId=... review link, drafts a concise title and responsibility summary from memory evidence, and asks the user to confirm both the profile and publication selection together. If an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. Already-flagged memories are identified separately and require extra attention before publication. This tool never publishes.",
528
+ description: "Prepare a no-publication preview of up to 50 owned memories for group publication. For encrypted accounts the local bridge must be unlocked. The host agent selects exact work-related memory ids, gives each candidate a canonical https://echoknows.com/memory/<memory-id> review link labeled with its key, drafts a concise title and responsibility summary from memory evidence, and asks the user to confirm both the profile and publication selection together. The site resolves owners to their private timeline. If an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. Already-flagged memories are identified separately and require extra attention before publication. This tool never publishes.",
496
529
  inputSchema: {
497
530
  type: "object",
498
531
  properties: {
@@ -508,7 +541,7 @@ export function listToolSpecs(opts = {}) {
508
541
  },
509
542
  {
510
543
  name: canonicalToolNames.flagPublicationAttention,
511
- description: "Mark exact owned memories for extra attention during future group publication. First search the user's own memories, show the matching memory IDs, summaries, and owner-only https://echoknows.com/memories/timeline?memoryId=... review links, and obtain explicit confirmation. This only records a safety flag; it does not publish, decrypt, change visibility, or retract an existing group snapshot.",
544
+ description: "Mark exact owned memories for extra attention during future group publication. First search the user's own memories, show the matching memory IDs, summaries, and canonical https://echoknows.com/memory/<memory-id> review links labeled with their keys, and obtain explicit confirmation. The site resolves owners to their private timeline. This only records a safety flag; it does not publish, decrypt, change visibility, or retract an existing group snapshot.",
512
545
  inputSchema: {
513
546
  type: "object",
514
547
  properties: {
@@ -548,7 +581,7 @@ export function listToolSpecs(opts = {}) {
548
581
  },
549
582
  {
550
583
  name: canonicalToolNames.completeGroupPublication,
551
- description: "Publish only the exact memory ids from a prepared scan after the user explicitly confirms the preview. confirmed must be true. Any flagged memories must be called out separately; state that nothing has been published yet and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories. Never auto-flag inferred sensitivity. Publish flagged memories only after extra user acknowledgement with their exact IDs in acknowledgedFlaggedMemoryIds. Completing an empty selection safely advances the scan cursor without publishing.",
584
+ description: "Publish only exact memory ids from a prepared scan after the user approves the preview with confirmed=true. This manual workflow is separate from session sharing. Explicitly confirmed flagged memories require their exact IDs in acknowledgedFlaggedMemoryIds. If an unflagged candidate appears sensitive, offer to first search for and mark similar sensitive owned memories for publication attention. Never auto-flag inferred sensitivity. Completing an empty selection safely advances the scan cursor.",
552
585
  inputSchema: {
553
586
  type: "object",
554
587
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.26",
3
+ "version": "1.4.27",
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",
@@ -26,7 +26,8 @@
26
26
  "test:registry": "node test/registry-artifact.test.mjs",
27
27
  "test:registry-ui": "npm run build && node test/registry-ui.test.mjs",
28
28
  "test:ui": "npm run build && node test/setup-ui.test.mjs",
29
- "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs",
29
+ "test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
30
+ "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs && node test/save-checkpoint-hook.test.mjs",
30
31
  "prepack": "npm run build && node scripts/bundle-city.mjs"
31
32
  },
32
33
  "dependencies": {
@@ -12,3 +12,13 @@ Use the `save_conversation` tool from the `echomem` MCP server.
12
12
  3. Use `passthrough: true` only when the user explicitly needs a verbatim session capsule.
13
13
  4. Never include secrets, credentials, or unnecessary sensitive content.
14
14
  5. Confirm what EchoMem saved. If saving fails, report the failure without switching providers silently.
15
+ 6. Before a final response, use this workflow proactively when the task produced a durable decision,
16
+ confirmed preference, implementation, resolved bug, commit, passing verification, release, or
17
+ project milestone. A private checkpoint does not require a separate user request. Skip trivial
18
+ read-only work and incidental chatter.
19
+ 7. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a
20
+ qualifying checkpoint.
21
+ 8. For a user with a company group, call `get_group_session_sharing` near session start or after the
22
+ first qualifying private save. If unset, ask once whether memories saved from this session should be
23
+ shared, then call `set_group_session_sharing` with the explicit Yes/No answer. A Yes decision syncs
24
+ eligible memories after private persistence; flagged memories remain private.
@@ -13,5 +13,13 @@ This project has EchoMem connected — the user's long-term memory across all th
13
13
  "the usual", "our convention").
14
14
  - Treat recalled memories as authoritative context, but **cite them** (they carry provenance —
15
15
  date + source) so the user can verify.
16
- - To persist a new decision/constraint/resolved-bug, use `save_conversation` **only when the user
17
- explicitly asks** ("remember this"). Never auto-save the transcript.
16
+ - Before the final response for work that produced a durable decision, confirmed preference,
17
+ implementation, resolved bug, commit, passing verification, release, or project milestone, call
18
+ `save_conversation`. This private checkpoint does not require a separate user request. Save a
19
+ concise structured outcome rather than the full transcript. Never save secrets, credentials,
20
+ incidental chatter, or trivial read-only work. This does not authorize company-group publication.
21
+ - For a user with a company group, call `get_group_session_sharing` near session start or after the
22
+ first qualifying private save. If this exact session has no decision, ask once whether memories
23
+ saved from it should be shared, then store only the explicit Yes/No answer with
24
+ `set_group_session_sharing`. A Yes decision syncs eligible memories after private persistence;
25
+ flagged memories stay private.