@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.
- package/dist/context-analysis/vendored-canonical.js +54 -11
- package/dist/forensics.js +24 -4
- package/dist/hud/cli.js +18 -1
- package/dist/hud/hooks.js +59 -0
- package/dist/index.js +111 -16
- package/dist/package-metadata.js +5 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +86 -15
- package/dist/setup-page/client-extraction.js +313 -145
- package/dist/setup-page/client-lifecycle.js +10 -11
- package/dist/setup-page/client-report-audit.js +3 -2
- package/dist/setup-page/client-report-city.js +36 -40
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +568 -150
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +143 -10
- package/dist/v1-contract.js +39 -6
- package/package.json +3 -2
- package/templates/codex-skills/echomem-save/SKILL.md +10 -0
- package/templates/echomem-recall.md +10 -2
package/dist/setup-preview.js
CHANGED
|
@@ -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: "
|
|
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-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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}
|
|
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
|
-
"-
|
|
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
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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({
|
|
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
|
-
|
|
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}
|
package/dist/v1-contract.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
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
|
-
-
|
|
17
|
-
|
|
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.
|