@echomem/mcp 1.4.27 → 1.4.29
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/codex-session-files.js +20 -0
- package/dist/forensics.js +1 -1
- package/dist/index.js +26 -10
- package/dist/migrate.js +24 -11
- package/dist/package-metadata.js +2 -0
- package/dist/setup-page/client-core.js +24 -24
- package/dist/setup-page/client-extraction.js +217 -76
- package/dist/setup-page/client-report-city.js +142 -8
- package/dist/setup-page/styles-extraction.js +37 -0
- package/dist/setup-page/styles-mvp.js +239 -7
- package/dist/setup-page/styles-website-alignment.js +8 -3
- package/dist/setup-preview.js +11 -0
- package/dist/setup.js +261 -35
- package/dist/v1-contract.js +10 -9
- package/package.json +1 -1
- package/templates/codex-skills/echomem-search/SKILL.md +3 -2
package/dist/setup.js
CHANGED
|
@@ -25,8 +25,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
25
25
|
import axios from "axios";
|
|
26
26
|
import { KeyStore } from "./keystore.js";
|
|
27
27
|
import { fetchEncryptionConfig, deriveAndVerifyKey, setupNewEncryptionKey, verifyKeyB64 } from "./encryption.js";
|
|
28
|
-
import {
|
|
29
|
-
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus,
|
|
28
|
+
import { runReport, buildStatsPayload } from "./report.js";
|
|
29
|
+
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
|
|
30
30
|
import { syncCodexUsage } from "./codex-sync.js";
|
|
31
31
|
import { renderSetupPage } from "./setup-page.js";
|
|
32
32
|
import { parseSetupPreviewState } from "./setup-preview.js";
|
|
@@ -389,6 +389,7 @@ 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
|
+
'- If the final user-facing answer materially relies on one or more EchoMem memories, end it with a compact `EchoMem sources:` list containing only the memories actually used. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.',
|
|
392
393
|
"- 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
394
|
"- 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.",
|
|
394
395
|
"- 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.",
|
|
@@ -1145,6 +1146,155 @@ function discoverMigratableSessionsOffThread() {
|
|
|
1145
1146
|
});
|
|
1146
1147
|
});
|
|
1147
1148
|
}
|
|
1149
|
+
export function discoverMigratableFastOffThread(opts = {}) {
|
|
1150
|
+
const migrateUrl = runtimeModuleUrl("migrate");
|
|
1151
|
+
const serializedOpts = JSON.stringify(opts);
|
|
1152
|
+
const code = `
|
|
1153
|
+
import { parentPort } from "node:worker_threads";
|
|
1154
|
+
import { discoverMigratableFastDiscovery } from ${JSON.stringify(migrateUrl)};
|
|
1155
|
+
|
|
1156
|
+
try {
|
|
1157
|
+
parentPort?.postMessage({ ok: true, discovery: discoverMigratableFastDiscovery(${serializedOpts}) });
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
parentPort?.postMessage({
|
|
1160
|
+
ok: false,
|
|
1161
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1162
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
`;
|
|
1166
|
+
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1167
|
+
return new Promise((resolve, reject) => {
|
|
1168
|
+
let settled = false;
|
|
1169
|
+
worker.once("message", (message) => {
|
|
1170
|
+
settled = true;
|
|
1171
|
+
const msg = message;
|
|
1172
|
+
if (msg.ok === true && msg.discovery && typeof msg.discovery === "object") {
|
|
1173
|
+
resolve(msg.discovery);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const err = new Error(typeof msg.message === "string" ? msg.message : "Quick local discovery failed");
|
|
1177
|
+
if (typeof msg.stack === "string")
|
|
1178
|
+
err.stack = msg.stack;
|
|
1179
|
+
reject(err);
|
|
1180
|
+
});
|
|
1181
|
+
worker.once("error", (error) => {
|
|
1182
|
+
if (settled)
|
|
1183
|
+
return;
|
|
1184
|
+
settled = true;
|
|
1185
|
+
reject(error);
|
|
1186
|
+
});
|
|
1187
|
+
worker.once("exit", (code) => {
|
|
1188
|
+
if (settled)
|
|
1189
|
+
return;
|
|
1190
|
+
settled = true;
|
|
1191
|
+
reject(new Error(`Quick local discovery worker exited (code ${code}) without a result`));
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
/** Build the full local-history dashboard payload away from the callback server's event loop.
|
|
1196
|
+
* `collect()` can synchronously parse hundreds of JSONL files for tens of seconds; doing that on
|
|
1197
|
+
* the bridge thread prevents even localhost actions such as account switch from receiving a reply. */
|
|
1198
|
+
export function buildCollectedStatsPayloadOffThread(inject) {
|
|
1199
|
+
const reportUrl = runtimeModuleUrl("report");
|
|
1200
|
+
const serializedInject = JSON.stringify(inject);
|
|
1201
|
+
const code = `
|
|
1202
|
+
import { parentPort } from "node:worker_threads";
|
|
1203
|
+
import { collect, buildStatsPayload } from ${JSON.stringify(reportUrl)};
|
|
1204
|
+
|
|
1205
|
+
try {
|
|
1206
|
+
const payload = await buildStatsPayload(collect(), ${serializedInject});
|
|
1207
|
+
parentPort?.postMessage({ ok: true, payload });
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
parentPort?.postMessage({
|
|
1210
|
+
ok: false,
|
|
1211
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1212
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
`;
|
|
1216
|
+
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1217
|
+
return new Promise((resolve, reject) => {
|
|
1218
|
+
let settled = false;
|
|
1219
|
+
const finish = (result) => {
|
|
1220
|
+
if (settled)
|
|
1221
|
+
return;
|
|
1222
|
+
settled = true;
|
|
1223
|
+
void worker.terminate();
|
|
1224
|
+
if (result.ok)
|
|
1225
|
+
resolve(result.payload);
|
|
1226
|
+
else
|
|
1227
|
+
reject(result.error);
|
|
1228
|
+
};
|
|
1229
|
+
worker.once("message", (message) => {
|
|
1230
|
+
const msg = message;
|
|
1231
|
+
if (msg.ok === true) {
|
|
1232
|
+
finish({ ok: true, payload: msg.payload });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const error = new Error(typeof msg.message === "string" ? msg.message : "Full local stats worker failed");
|
|
1236
|
+
if (typeof msg.stack === "string")
|
|
1237
|
+
error.stack = msg.stack;
|
|
1238
|
+
finish({ ok: false, error });
|
|
1239
|
+
});
|
|
1240
|
+
worker.once("error", (error) => {
|
|
1241
|
+
finish({ ok: false, error });
|
|
1242
|
+
});
|
|
1243
|
+
worker.once("exit", (code) => {
|
|
1244
|
+
if (settled)
|
|
1245
|
+
return;
|
|
1246
|
+
finish({ ok: false, error: new Error(`Full local stats worker exited (code ${code}) without a result`) });
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
export function createLocalDiscoveryCache(loaders = {}) {
|
|
1251
|
+
const loadQuick = loaders.loadQuick ?? (() => discoverMigratableFastOffThread());
|
|
1252
|
+
const loadExact = loaders.loadExact ?? (() => discoverMigratableSessionsOffThread());
|
|
1253
|
+
let quick = null;
|
|
1254
|
+
let quickPromise = null;
|
|
1255
|
+
let exact = null;
|
|
1256
|
+
let exactPromise = null;
|
|
1257
|
+
return {
|
|
1258
|
+
getQuick() {
|
|
1259
|
+
if (quick)
|
|
1260
|
+
return Promise.resolve(quick);
|
|
1261
|
+
if (quickPromise)
|
|
1262
|
+
return quickPromise;
|
|
1263
|
+
quickPromise = loadQuick()
|
|
1264
|
+
.then((discovery) => {
|
|
1265
|
+
quick = discovery;
|
|
1266
|
+
return discovery;
|
|
1267
|
+
})
|
|
1268
|
+
.catch((error) => {
|
|
1269
|
+
quickPromise = null;
|
|
1270
|
+
throw error;
|
|
1271
|
+
});
|
|
1272
|
+
return quickPromise;
|
|
1273
|
+
},
|
|
1274
|
+
getExact() {
|
|
1275
|
+
if (exact)
|
|
1276
|
+
return Promise.resolve(exact);
|
|
1277
|
+
if (exactPromise)
|
|
1278
|
+
return exactPromise;
|
|
1279
|
+
exactPromise = loadExact()
|
|
1280
|
+
.then((discovery) => {
|
|
1281
|
+
exact = discovery;
|
|
1282
|
+
return discovery;
|
|
1283
|
+
})
|
|
1284
|
+
.catch((error) => {
|
|
1285
|
+
exactPromise = null;
|
|
1286
|
+
throw error;
|
|
1287
|
+
});
|
|
1288
|
+
return exactPromise;
|
|
1289
|
+
},
|
|
1290
|
+
peekExact() {
|
|
1291
|
+
return exact;
|
|
1292
|
+
},
|
|
1293
|
+
pendingExact() {
|
|
1294
|
+
return exactPromise;
|
|
1295
|
+
},
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1148
1298
|
function forensicStageLabel(stage) {
|
|
1149
1299
|
if (stage === "reading-transcripts")
|
|
1150
1300
|
return "Reading transcript files";
|
|
@@ -1166,7 +1316,9 @@ export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
|
1166
1316
|
try {
|
|
1167
1317
|
const report = await buildForensicReport({
|
|
1168
1318
|
includeLegacyGoldenStandard: false,
|
|
1169
|
-
onProgress: (done, total, stage, detail, overall) => parentPort?.postMessage({
|
|
1319
|
+
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1320
|
+
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1321
|
+
}),
|
|
1170
1322
|
});
|
|
1171
1323
|
parentPort?.postMessage({ ok: true, report });
|
|
1172
1324
|
} catch (error) {
|
|
@@ -1259,6 +1411,7 @@ function publicRunningForensicProgress(value) {
|
|
|
1259
1411
|
? Math.min(1, Math.max(0, candidate))
|
|
1260
1412
|
: 0);
|
|
1261
1413
|
const total = safeCount(progress.total);
|
|
1414
|
+
const stageTotal = safeCount(progress.stageTotal);
|
|
1262
1415
|
const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
|
|
1263
1416
|
const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
|
|
1264
1417
|
? rawStage
|
|
@@ -1269,6 +1422,8 @@ function publicRunningForensicProgress(value) {
|
|
|
1269
1422
|
total,
|
|
1270
1423
|
stage,
|
|
1271
1424
|
label: forensicStageLabel(stage),
|
|
1425
|
+
stageDone: stageTotal > 0 ? Math.min(safeCount(progress.stageDone), stageTotal) : 0,
|
|
1426
|
+
stageTotal,
|
|
1272
1427
|
overall: safeFraction(progress.overall),
|
|
1273
1428
|
elapsedMs: safeDuration(progress.elapsedMs),
|
|
1274
1429
|
stageElapsedMs: safeDuration(progress.stageElapsedMs),
|
|
@@ -1303,6 +1458,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1303
1458
|
let switchAccountUrl = "";
|
|
1304
1459
|
let connected = Boolean(opts.initialToken?.token);
|
|
1305
1460
|
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1461
|
+
let activeAccountEmail = "";
|
|
1306
1462
|
let pendingLocalAuth = null;
|
|
1307
1463
|
let reportConsentGranted = !requiresReportConsent;
|
|
1308
1464
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
@@ -1337,8 +1493,8 @@ export function startCallbackServer(opts = {}) {
|
|
|
1337
1493
|
return false;
|
|
1338
1494
|
return revokeDeviceToken(pending.token);
|
|
1339
1495
|
};
|
|
1340
|
-
const completeDeviceLogin = async (token) => {
|
|
1341
|
-
await authedAxios(token).post("/api/extension/mcp/local-auth/complete-device", {}, { timeout: 10_000 });
|
|
1496
|
+
const completeDeviceLogin = async (token, encryptionPreference = "maximum") => {
|
|
1497
|
+
await authedAxios(token).post("/api/extension/mcp/local-auth/complete-device", { encryptionPreference }, { timeout: 10_000 });
|
|
1342
1498
|
};
|
|
1343
1499
|
const close = () => {
|
|
1344
1500
|
if (timer)
|
|
@@ -1414,6 +1570,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1414
1570
|
const firstToken = !onToken.settled();
|
|
1415
1571
|
connected = true;
|
|
1416
1572
|
activeDeviceToken = token;
|
|
1573
|
+
activeAccountEmail = pendingLocalAuth?.email || activeAccountEmail;
|
|
1417
1574
|
pendingLocalAuth = null;
|
|
1418
1575
|
const callbackToken = { token, key };
|
|
1419
1576
|
if (firstToken) {
|
|
@@ -1571,6 +1728,38 @@ export function startCallbackServer(opts = {}) {
|
|
|
1571
1728
|
json(res, detail.status, { ok: false, error: detail.message });
|
|
1572
1729
|
}
|
|
1573
1730
|
};
|
|
1731
|
+
const handleLocalSkipEncryption = async (res, body) => {
|
|
1732
|
+
if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
|
|
1733
|
+
return;
|
|
1734
|
+
if (!pendingLocalAuth || pendingLocalAuth.expiresAtMs <= Date.now()) {
|
|
1735
|
+
await revokePendingLocalAuth();
|
|
1736
|
+
return void json(res, 409, {
|
|
1737
|
+
ok: false,
|
|
1738
|
+
error: "This local login expired. Send a new verification code.",
|
|
1739
|
+
reset: true,
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
if (pendingLocalAuth.mode !== "setup") {
|
|
1743
|
+
return void json(res, 409, {
|
|
1744
|
+
ok: false,
|
|
1745
|
+
error: "This account already uses encrypted memory. Enter the vault passphrase to continue.",
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
await completeDeviceLogin(pendingLocalAuth.token, "standard");
|
|
1750
|
+
resolveLocalToken(pendingLocalAuth.token, undefined);
|
|
1751
|
+
json(res, 200, {
|
|
1752
|
+
ok: true,
|
|
1753
|
+
connected: true,
|
|
1754
|
+
encryptionEnabled: false,
|
|
1755
|
+
encryptionPreference: "standard",
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
catch (error) {
|
|
1759
|
+
const detail = publicAxiosError(error, "Failed to continue without encryption");
|
|
1760
|
+
json(res, detail.status, { ok: false, error: detail.message });
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1574
1763
|
server = http.createServer((req, res) => {
|
|
1575
1764
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1576
1765
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -1635,6 +1824,18 @@ export function startCallbackServer(opts = {}) {
|
|
|
1635
1824
|
});
|
|
1636
1825
|
return;
|
|
1637
1826
|
}
|
|
1827
|
+
if (route === "/local-auth/skip-encryption" && req.method === "POST") {
|
|
1828
|
+
let body;
|
|
1829
|
+
try {
|
|
1830
|
+
body = await readJsonBody(req);
|
|
1831
|
+
}
|
|
1832
|
+
catch {
|
|
1833
|
+
text(res, 400, "bad json");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
await handleLocalSkipEncryption(res, body);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1638
1839
|
if (route === "/launch-agent" && req.method === "POST") {
|
|
1639
1840
|
let body;
|
|
1640
1841
|
try {
|
|
@@ -1771,6 +1972,9 @@ export function startCallbackServer(opts = {}) {
|
|
|
1771
1972
|
const plan = (asString(response.data?.plan) || "free").toLowerCase();
|
|
1772
1973
|
const trialUsed = response.data?.billing?.trialUsed === true;
|
|
1773
1974
|
const trialAvailable = response.data?.billing?.trialAvailable !== false;
|
|
1975
|
+
const accountEmail = asString(profileResponse?.data?.email) || activeAccountEmail;
|
|
1976
|
+
if (accountEmail)
|
|
1977
|
+
activeAccountEmail = accountEmail;
|
|
1774
1978
|
json(res, 200, {
|
|
1775
1979
|
plan,
|
|
1776
1980
|
paid: ["pro", "power", "team", "enterprise"].includes(plan),
|
|
@@ -1785,10 +1989,9 @@ export function startCallbackServer(opts = {}) {
|
|
|
1785
1989
|
memoryProcessingQuota: response.data?.memoryProcessingQuota ?? null,
|
|
1786
1990
|
memorySearchQuota: response.data?.memorySearchQuota ?? null,
|
|
1787
1991
|
activation: response.data?.activation ?? null,
|
|
1788
|
-
account:
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
avatarUrl: asString(profileResponse.data?.avatarUrl) || "",
|
|
1992
|
+
account: accountEmail ? {
|
|
1993
|
+
email: accountEmail,
|
|
1994
|
+
avatarUrl: asString(profileResponse?.data?.avatarUrl) || "",
|
|
1792
1995
|
} : null,
|
|
1793
1996
|
pricingUrl,
|
|
1794
1997
|
});
|
|
@@ -2083,22 +2286,25 @@ export function startCallbackServer(opts = {}) {
|
|
|
2083
2286
|
}
|
|
2084
2287
|
connected = false;
|
|
2085
2288
|
activeDeviceToken = "";
|
|
2289
|
+
activeAccountEmail = "";
|
|
2086
2290
|
const revokedPendingCredential = await revokePendingLocalAuth();
|
|
2087
2291
|
stats = null;
|
|
2088
2292
|
migrateStarted = false;
|
|
2089
2293
|
progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
2294
|
+
try {
|
|
2295
|
+
await logoutHandler?.();
|
|
2296
|
+
}
|
|
2297
|
+
catch (e) {
|
|
2298
|
+
console.error(`Could not reset local login state: ${e instanceof Error ? e.message : String(e)}`);
|
|
2299
|
+
}
|
|
2090
2300
|
json(res, 200, {
|
|
2091
2301
|
ok: true,
|
|
2302
|
+
connected: false,
|
|
2092
2303
|
authUrl,
|
|
2093
2304
|
switchAccountUrl: switchAccountUrl || authUrl,
|
|
2094
2305
|
localOnly: true,
|
|
2095
2306
|
revokedPendingCredential,
|
|
2096
2307
|
});
|
|
2097
|
-
Promise.resolve()
|
|
2098
|
-
.then(() => logoutHandler?.())
|
|
2099
|
-
.catch((e) => {
|
|
2100
|
-
console.error(`Could not reset local login state: ${e instanceof Error ? e.message : String(e)}`);
|
|
2101
|
-
});
|
|
2102
2308
|
return;
|
|
2103
2309
|
}
|
|
2104
2310
|
if (route === "/migrate" && req.method === "POST") {
|
|
@@ -2665,6 +2871,8 @@ async function cmdOnboarding(flags) {
|
|
|
2665
2871
|
total: 0,
|
|
2666
2872
|
stage: forensicStage,
|
|
2667
2873
|
label: forensicStageLabel(forensicStage),
|
|
2874
|
+
stageDone: 0,
|
|
2875
|
+
stageTotal: 0,
|
|
2668
2876
|
overall: 0,
|
|
2669
2877
|
elapsedMs: 0,
|
|
2670
2878
|
stageElapsedMs: 0,
|
|
@@ -2678,7 +2886,18 @@ async function cmdOnboarding(flags) {
|
|
|
2678
2886
|
requireReportConsent: true,
|
|
2679
2887
|
getStats: () => stats,
|
|
2680
2888
|
getReport: () => forensicReport,
|
|
2681
|
-
getReportProgress: () =>
|
|
2889
|
+
getReportProgress: () => {
|
|
2890
|
+
if (forensicProgress.status !== "running")
|
|
2891
|
+
return forensicProgress;
|
|
2892
|
+
const now = Date.now();
|
|
2893
|
+
const sinceWorkerUpdate = Math.max(0, now - forensicProgress.updatedAt);
|
|
2894
|
+
return {
|
|
2895
|
+
...forensicProgress,
|
|
2896
|
+
elapsedMs: forensicProgress.elapsedMs + sinceWorkerUpdate,
|
|
2897
|
+
stageElapsedMs: forensicProgress.stageElapsedMs + sinceWorkerUpdate,
|
|
2898
|
+
updatedAt: now,
|
|
2899
|
+
};
|
|
2900
|
+
},
|
|
2682
2901
|
onReportConsent: (allowed) => {
|
|
2683
2902
|
if (!allowed) {
|
|
2684
2903
|
forensicConsent = "declined";
|
|
@@ -2688,6 +2907,8 @@ async function cmdOnboarding(flags) {
|
|
|
2688
2907
|
total: 0,
|
|
2689
2908
|
stage: "failed",
|
|
2690
2909
|
label: "Local scan skipped",
|
|
2910
|
+
stageDone: 0,
|
|
2911
|
+
stageTotal: 0,
|
|
2691
2912
|
overall: 0,
|
|
2692
2913
|
elapsedMs: Date.now() - forensicStartedAt,
|
|
2693
2914
|
stageElapsedMs: Date.now() - forensicStageStartedAt,
|
|
@@ -2710,6 +2931,8 @@ async function cmdOnboarding(flags) {
|
|
|
2710
2931
|
total: 0,
|
|
2711
2932
|
stage: forensicStage,
|
|
2712
2933
|
label: forensicStageLabel(forensicStage),
|
|
2934
|
+
stageDone: 0,
|
|
2935
|
+
stageTotal: 0,
|
|
2713
2936
|
overall: 0,
|
|
2714
2937
|
elapsedMs: now - forensicStartedAt,
|
|
2715
2938
|
stageElapsedMs: 0,
|
|
@@ -2744,6 +2967,12 @@ async function cmdOnboarding(flags) {
|
|
|
2744
2967
|
stage: forensicStage,
|
|
2745
2968
|
label: forensicStageLabel(forensicStage),
|
|
2746
2969
|
detail: progress.detail,
|
|
2970
|
+
stageDone: typeof progress.stageDone === "number" && Number.isFinite(progress.stageDone)
|
|
2971
|
+
? Math.max(0, Math.floor(progress.stageDone))
|
|
2972
|
+
: 0,
|
|
2973
|
+
stageTotal: typeof progress.stageTotal === "number" && Number.isFinite(progress.stageTotal)
|
|
2974
|
+
? Math.max(0, Math.floor(progress.stageTotal))
|
|
2975
|
+
: 0,
|
|
2747
2976
|
overall: forensicOverall,
|
|
2748
2977
|
elapsedMs: now - forensicStartedAt,
|
|
2749
2978
|
stageElapsedMs: now - forensicStageStartedAt,
|
|
@@ -2761,6 +2990,8 @@ async function cmdOnboarding(flags) {
|
|
|
2761
2990
|
total: forensicProgress.total,
|
|
2762
2991
|
stage: "failed",
|
|
2763
2992
|
label: "Local scan failed",
|
|
2993
|
+
stageDone: forensicProgress.stageDone,
|
|
2994
|
+
stageTotal: forensicProgress.stageTotal,
|
|
2764
2995
|
overall: forensicOverall,
|
|
2765
2996
|
elapsedMs: now - forensicStartedAt,
|
|
2766
2997
|
stageElapsedMs: now - forensicStageStartedAt,
|
|
@@ -2799,13 +3030,22 @@ async function cmdOnboarding(flags) {
|
|
|
2799
3030
|
let exactDiscovery = Promise.resolve(null);
|
|
2800
3031
|
let refreshGeneration = 0;
|
|
2801
3032
|
let latestPendingEstimate = 0;
|
|
3033
|
+
// Local discovery is account-independent. Keep one snapshot for this onboarding process and
|
|
3034
|
+
// overlay each connected account's processed keys instead of rereading every JSONL on switch.
|
|
3035
|
+
const localDiscovery = createLocalDiscoveryCache({
|
|
3036
|
+
loadExact: async () => {
|
|
3037
|
+
// Publish the quick metadata result before starting the heavier exact worker.
|
|
3038
|
+
await delay(250);
|
|
3039
|
+
return discoverMigratableSessionsOffThread();
|
|
3040
|
+
},
|
|
3041
|
+
});
|
|
2802
3042
|
// The account's already-imported keys, shared so the /migrate sizing can assemble ONLY pending sessions.
|
|
2803
3043
|
let lastProcessedImportKeys = null;
|
|
2804
3044
|
const resetLocalLoginState = () => {
|
|
2805
3045
|
refreshGeneration++;
|
|
2806
3046
|
stats = null;
|
|
2807
3047
|
disc = null;
|
|
2808
|
-
exactDiscovery = Promise.resolve(
|
|
3048
|
+
exactDiscovery = localDiscovery.pendingExact() ?? Promise.resolve(localDiscovery.peekExact());
|
|
2809
3049
|
latestPendingEstimate = 0;
|
|
2810
3050
|
lastProcessedImportKeys = null;
|
|
2811
3051
|
srv.setStats(null);
|
|
@@ -2823,7 +3063,9 @@ async function cmdOnboarding(flags) {
|
|
|
2823
3063
|
const generation = ++refreshGeneration;
|
|
2824
3064
|
lastProcessedImportKeys = null; // shared with /migrate so it can assemble only the pending sessions
|
|
2825
3065
|
let importStatusUnavailable = false;
|
|
2826
|
-
const quickDiscovery =
|
|
3066
|
+
const quickDiscovery = await localDiscovery.getQuick();
|
|
3067
|
+
if (generation !== refreshGeneration)
|
|
3068
|
+
return;
|
|
2827
3069
|
const quick = summarizeFastMigratableDiscovery(quickDiscovery);
|
|
2828
3070
|
let migratable = migratableFromFastSummary(quick);
|
|
2829
3071
|
latestPendingEstimate = migratable.pending;
|
|
@@ -2931,23 +3173,7 @@ async function cmdOnboarding(flags) {
|
|
|
2931
3173
|
console.error(`Could not check this EchoMem account's import status quickly: ${e instanceof Error ? e.message : String(e)}`);
|
|
2932
3174
|
}
|
|
2933
3175
|
});
|
|
2934
|
-
exactDiscovery =
|
|
2935
|
-
const timer = setTimeout(() => {
|
|
2936
|
-
void (async () => {
|
|
2937
|
-
try {
|
|
2938
|
-
// Start the local sizing pass without waiting on cloud/account status. The
|
|
2939
|
-
// account check is useful for tighter counts, but extraction can safely start
|
|
2940
|
-
// from local candidates because the import path skips true duplicates.
|
|
2941
|
-
await delay(250);
|
|
2942
|
-
resolve(await discoverMigratableSessionsOffThread());
|
|
2943
|
-
}
|
|
2944
|
-
catch (e) {
|
|
2945
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
2946
|
-
}
|
|
2947
|
-
})();
|
|
2948
|
-
}, 250);
|
|
2949
|
-
timer.unref?.();
|
|
2950
|
-
}).then(async (exact) => {
|
|
3176
|
+
exactDiscovery = localDiscovery.getExact().then(async (exact) => {
|
|
2951
3177
|
if (generation !== refreshGeneration)
|
|
2952
3178
|
return disc;
|
|
2953
3179
|
const initialExact = lastProcessedImportKeys
|
|
@@ -3029,7 +3255,7 @@ async function cmdOnboarding(flags) {
|
|
|
3029
3255
|
failed: 0,
|
|
3030
3256
|
extracted: 0,
|
|
3031
3257
|
});
|
|
3032
|
-
const fullPayload = withCandidateSessions(await
|
|
3258
|
+
const fullPayload = withCandidateSessions(await buildCollectedStatsPayloadOffThread({
|
|
3033
3259
|
sessions: sessionSummary,
|
|
3034
3260
|
migratable,
|
|
3035
3261
|
discovery: { phase: "full", exact: true },
|
package/dist/v1-contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { withMcpVersion } from "./package-metadata.js";
|
|
2
|
+
import { MEMORY_CITATION_INSTRUCTION, withMcpVersion } from "./package-metadata.js";
|
|
3
3
|
export const canonicalToolNames = {
|
|
4
4
|
search: "search_memories",
|
|
5
5
|
save: "save_conversation",
|
|
@@ -204,6 +204,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
204
204
|
const updateNotice = opts.updateNotice?.trim();
|
|
205
205
|
const recallPlanNote = "Available on every plan: Free includes 100 searches each week, Pro includes 500, and Power includes 2,000.";
|
|
206
206
|
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\".";
|
|
207
|
+
const memoryCitationInstruction = MEMORY_CITATION_INSTRUCTION;
|
|
207
208
|
const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
|
|
208
209
|
const mapSection = map
|
|
209
210
|
? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
|
|
@@ -211,7 +212,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
211
212
|
return [
|
|
212
213
|
{
|
|
213
214
|
name: canonicalToolNames.search,
|
|
214
|
-
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. ${recallPlanNote} ${searchBillingReplyInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
|
|
215
|
+
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. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
|
|
215
216
|
inputSchema: {
|
|
216
217
|
type: "object",
|
|
217
218
|
properties: {
|
|
@@ -229,7 +230,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
229
230
|
},
|
|
230
231
|
{
|
|
231
232
|
name: "search_memories_by_description_semantic",
|
|
232
|
-
description: `Legacy alias for search_memories. ${recallPlanNote} ${searchBillingReplyInstruction}`,
|
|
233
|
+
description: `Legacy alias for search_memories. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}`,
|
|
233
234
|
inputSchema: {
|
|
234
235
|
type: "object",
|
|
235
236
|
properties: {
|
|
@@ -280,7 +281,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
280
281
|
},
|
|
281
282
|
{
|
|
282
283
|
name: canonicalToolNames.timeRange,
|
|
283
|
-
description: `Retrieve memories within a specific date range. ${recallPlanNote} Current time: ${currentTime}.`,
|
|
284
|
+
description: `Retrieve memories within a specific date range. ${recallPlanNote} ${memoryCitationInstruction} Current time: ${currentTime}.`,
|
|
284
285
|
inputSchema: {
|
|
285
286
|
type: "object",
|
|
286
287
|
properties: {
|
|
@@ -298,7 +299,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
298
299
|
},
|
|
299
300
|
{
|
|
300
301
|
name: canonicalToolNames.keywords,
|
|
301
|
-
description: `Search memories based on keywords in keys field. Pass keywords as valid JSON: preferably an array of quoted strings, for example {"keywords":["flow-lab","flow.html","Rive"],"limit":8}. A comma-separated JSON string is also accepted as a compatibility fallback. Never emit bare comma-separated tokens. ${recallPlanNote}`,
|
|
302
|
+
description: `Search memories based on keywords in keys field. Pass keywords as valid JSON: preferably an array of quoted strings, for example {"keywords":["flow-lab","flow.html","Rive"],"limit":8}. A comma-separated JSON string is also accepted as a compatibility fallback. Never emit bare comma-separated tokens. ${recallPlanNote} ${memoryCitationInstruction}`,
|
|
302
303
|
inputSchema: {
|
|
303
304
|
type: "object",
|
|
304
305
|
properties: {
|
|
@@ -384,7 +385,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
384
385
|
},
|
|
385
386
|
{
|
|
386
387
|
name: canonicalToolNames.others,
|
|
387
|
-
description:
|
|
388
|
+
description: `Search public memories from accepted friends or people who share your company group. For onboarding and division-of-work questions, call get_group_context first, then use this tool for current evidence. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}`,
|
|
388
389
|
inputSchema: {
|
|
389
390
|
type: "object",
|
|
390
391
|
properties: {
|
|
@@ -429,7 +430,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
429
430
|
},
|
|
430
431
|
{
|
|
431
432
|
name: canonicalToolNames.publicMemory,
|
|
432
|
-
description:
|
|
433
|
+
description: `Fetch one public memory by id when its owner is an accepted friend or shares your company group. If the caller is not the owner, EchoMem records the access in memory_views. ${memoryCitationInstruction}`,
|
|
433
434
|
inputSchema: {
|
|
434
435
|
type: "object",
|
|
435
436
|
properties: {
|
|
@@ -685,7 +686,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
685
686
|
},
|
|
686
687
|
{
|
|
687
688
|
name: canonicalToolNames.getByContext,
|
|
688
|
-
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${recallPlanNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
|
|
689
|
+
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${recallPlanNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. ${memoryCitationInstruction} Current time: ${currentTime}.`),
|
|
689
690
|
inputSchema: {
|
|
690
691
|
type: "object",
|
|
691
692
|
properties: {
|
|
@@ -702,7 +703,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
702
703
|
},
|
|
703
704
|
{
|
|
704
705
|
name: canonicalToolNames.checkpointByContext,
|
|
705
|
-
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. Current time: ${currentTime}.`),
|
|
706
|
+
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. ${memoryCitationInstruction} Current time: ${currentTime}.`),
|
|
706
707
|
inputSchema: {
|
|
707
708
|
type: "object",
|
|
708
709
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.29",
|
|
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",
|
|
@@ -10,5 +10,6 @@ Use the `echomem` MCP server as the source of long-term memory.
|
|
|
10
10
|
1. Call `search_memories` with a concise query describing the context needed. Keep `includeAnswer` false unless an EchoMem-generated synthesis is specifically useful.
|
|
11
11
|
2. Use `get_memories_by_time_range` for explicit dates, `search_memories_by_keywords` for exact terms, and `get_checkpoint_by_context` when a carryover references an EchoMem checkpoint.
|
|
12
12
|
3. Distinguish recalled facts from inference and preserve dates or provenance returned by EchoMem.
|
|
13
|
-
4. If EchoMem
|
|
14
|
-
5. If EchoMem
|
|
13
|
+
4. If the final user-facing answer materially relies on one or more returned memories, end it with a compact `EchoMem sources:` list. Include only memories actually used, with each memory key linked to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories merely retrieved; omit the section when none informed the answer.
|
|
14
|
+
5. If EchoMem returns an action-required URL, give the user that exact URL and explanation.
|
|
15
|
+
6. If EchoMem is unavailable, say so. Do not switch to another memory provider unless the user requests it.
|