@echomem/mcp 1.4.26 → 1.4.28
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/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/migrate.js +24 -11
- package/dist/package-metadata.js +5 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +87 -15
- package/dist/setup-page/client-extraction.js +508 -207
- 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 +168 -38
- package/dist/setup-page/styles-extraction.js +37 -0
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +751 -151
- package/dist/setup-page/styles-website-alignment.js +8 -3
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +402 -44
- 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.js
CHANGED
|
@@ -25,13 +25,13 @@ 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";
|
|
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.",
|
|
@@ -1143,6 +1145,155 @@ function discoverMigratableSessionsOffThread() {
|
|
|
1143
1145
|
});
|
|
1144
1146
|
});
|
|
1145
1147
|
}
|
|
1148
|
+
export function discoverMigratableFastOffThread(opts = {}) {
|
|
1149
|
+
const migrateUrl = runtimeModuleUrl("migrate");
|
|
1150
|
+
const serializedOpts = JSON.stringify(opts);
|
|
1151
|
+
const code = `
|
|
1152
|
+
import { parentPort } from "node:worker_threads";
|
|
1153
|
+
import { discoverMigratableFastDiscovery } from ${JSON.stringify(migrateUrl)};
|
|
1154
|
+
|
|
1155
|
+
try {
|
|
1156
|
+
parentPort?.postMessage({ ok: true, discovery: discoverMigratableFastDiscovery(${serializedOpts}) });
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
parentPort?.postMessage({
|
|
1159
|
+
ok: false,
|
|
1160
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1161
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
`;
|
|
1165
|
+
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1166
|
+
return new Promise((resolve, reject) => {
|
|
1167
|
+
let settled = false;
|
|
1168
|
+
worker.once("message", (message) => {
|
|
1169
|
+
settled = true;
|
|
1170
|
+
const msg = message;
|
|
1171
|
+
if (msg.ok === true && msg.discovery && typeof msg.discovery === "object") {
|
|
1172
|
+
resolve(msg.discovery);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const err = new Error(typeof msg.message === "string" ? msg.message : "Quick local discovery failed");
|
|
1176
|
+
if (typeof msg.stack === "string")
|
|
1177
|
+
err.stack = msg.stack;
|
|
1178
|
+
reject(err);
|
|
1179
|
+
});
|
|
1180
|
+
worker.once("error", (error) => {
|
|
1181
|
+
if (settled)
|
|
1182
|
+
return;
|
|
1183
|
+
settled = true;
|
|
1184
|
+
reject(error);
|
|
1185
|
+
});
|
|
1186
|
+
worker.once("exit", (code) => {
|
|
1187
|
+
if (settled)
|
|
1188
|
+
return;
|
|
1189
|
+
settled = true;
|
|
1190
|
+
reject(new Error(`Quick local discovery worker exited (code ${code}) without a result`));
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
/** Build the full local-history dashboard payload away from the callback server's event loop.
|
|
1195
|
+
* `collect()` can synchronously parse hundreds of JSONL files for tens of seconds; doing that on
|
|
1196
|
+
* the bridge thread prevents even localhost actions such as account switch from receiving a reply. */
|
|
1197
|
+
export function buildCollectedStatsPayloadOffThread(inject) {
|
|
1198
|
+
const reportUrl = runtimeModuleUrl("report");
|
|
1199
|
+
const serializedInject = JSON.stringify(inject);
|
|
1200
|
+
const code = `
|
|
1201
|
+
import { parentPort } from "node:worker_threads";
|
|
1202
|
+
import { collect, buildStatsPayload } from ${JSON.stringify(reportUrl)};
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const payload = await buildStatsPayload(collect(), ${serializedInject});
|
|
1206
|
+
parentPort?.postMessage({ ok: true, payload });
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
parentPort?.postMessage({
|
|
1209
|
+
ok: false,
|
|
1210
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1211
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
`;
|
|
1215
|
+
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
1216
|
+
return new Promise((resolve, reject) => {
|
|
1217
|
+
let settled = false;
|
|
1218
|
+
const finish = (result) => {
|
|
1219
|
+
if (settled)
|
|
1220
|
+
return;
|
|
1221
|
+
settled = true;
|
|
1222
|
+
void worker.terminate();
|
|
1223
|
+
if (result.ok)
|
|
1224
|
+
resolve(result.payload);
|
|
1225
|
+
else
|
|
1226
|
+
reject(result.error);
|
|
1227
|
+
};
|
|
1228
|
+
worker.once("message", (message) => {
|
|
1229
|
+
const msg = message;
|
|
1230
|
+
if (msg.ok === true) {
|
|
1231
|
+
finish({ ok: true, payload: msg.payload });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const error = new Error(typeof msg.message === "string" ? msg.message : "Full local stats worker failed");
|
|
1235
|
+
if (typeof msg.stack === "string")
|
|
1236
|
+
error.stack = msg.stack;
|
|
1237
|
+
finish({ ok: false, error });
|
|
1238
|
+
});
|
|
1239
|
+
worker.once("error", (error) => {
|
|
1240
|
+
finish({ ok: false, error });
|
|
1241
|
+
});
|
|
1242
|
+
worker.once("exit", (code) => {
|
|
1243
|
+
if (settled)
|
|
1244
|
+
return;
|
|
1245
|
+
finish({ ok: false, error: new Error(`Full local stats worker exited (code ${code}) without a result`) });
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
export function createLocalDiscoveryCache(loaders = {}) {
|
|
1250
|
+
const loadQuick = loaders.loadQuick ?? (() => discoverMigratableFastOffThread());
|
|
1251
|
+
const loadExact = loaders.loadExact ?? (() => discoverMigratableSessionsOffThread());
|
|
1252
|
+
let quick = null;
|
|
1253
|
+
let quickPromise = null;
|
|
1254
|
+
let exact = null;
|
|
1255
|
+
let exactPromise = null;
|
|
1256
|
+
return {
|
|
1257
|
+
getQuick() {
|
|
1258
|
+
if (quick)
|
|
1259
|
+
return Promise.resolve(quick);
|
|
1260
|
+
if (quickPromise)
|
|
1261
|
+
return quickPromise;
|
|
1262
|
+
quickPromise = loadQuick()
|
|
1263
|
+
.then((discovery) => {
|
|
1264
|
+
quick = discovery;
|
|
1265
|
+
return discovery;
|
|
1266
|
+
})
|
|
1267
|
+
.catch((error) => {
|
|
1268
|
+
quickPromise = null;
|
|
1269
|
+
throw error;
|
|
1270
|
+
});
|
|
1271
|
+
return quickPromise;
|
|
1272
|
+
},
|
|
1273
|
+
getExact() {
|
|
1274
|
+
if (exact)
|
|
1275
|
+
return Promise.resolve(exact);
|
|
1276
|
+
if (exactPromise)
|
|
1277
|
+
return exactPromise;
|
|
1278
|
+
exactPromise = loadExact()
|
|
1279
|
+
.then((discovery) => {
|
|
1280
|
+
exact = discovery;
|
|
1281
|
+
return discovery;
|
|
1282
|
+
})
|
|
1283
|
+
.catch((error) => {
|
|
1284
|
+
exactPromise = null;
|
|
1285
|
+
throw error;
|
|
1286
|
+
});
|
|
1287
|
+
return exactPromise;
|
|
1288
|
+
},
|
|
1289
|
+
peekExact() {
|
|
1290
|
+
return exact;
|
|
1291
|
+
},
|
|
1292
|
+
pendingExact() {
|
|
1293
|
+
return exactPromise;
|
|
1294
|
+
},
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1146
1297
|
function forensicStageLabel(stage) {
|
|
1147
1298
|
if (stage === "reading-transcripts")
|
|
1148
1299
|
return "Reading transcript files";
|
|
@@ -1164,7 +1315,9 @@ export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
|
1164
1315
|
try {
|
|
1165
1316
|
const report = await buildForensicReport({
|
|
1166
1317
|
includeLegacyGoldenStandard: false,
|
|
1167
|
-
onProgress: (done, total, stage, detail) => parentPort?.postMessage({
|
|
1318
|
+
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1319
|
+
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1320
|
+
}),
|
|
1168
1321
|
});
|
|
1169
1322
|
parentPort?.postMessage({ ok: true, report });
|
|
1170
1323
|
} catch (error) {
|
|
@@ -1253,7 +1406,11 @@ function publicRunningForensicProgress(value) {
|
|
|
1253
1406
|
const safeDuration = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0
|
|
1254
1407
|
? candidate
|
|
1255
1408
|
: 0);
|
|
1409
|
+
const safeFraction = (candidate) => (typeof candidate === "number" && Number.isFinite(candidate)
|
|
1410
|
+
? Math.min(1, Math.max(0, candidate))
|
|
1411
|
+
: 0);
|
|
1256
1412
|
const total = safeCount(progress.total);
|
|
1413
|
+
const stageTotal = safeCount(progress.stageTotal);
|
|
1257
1414
|
const rawStage = typeof progress.stage === "string" ? progress.stage : "starting";
|
|
1258
1415
|
const stage = ["starting", "reading-transcripts", "building-summary", "classifying-repeated-context", "finalizing-report"].includes(rawStage)
|
|
1259
1416
|
? rawStage
|
|
@@ -1264,6 +1421,9 @@ function publicRunningForensicProgress(value) {
|
|
|
1264
1421
|
total,
|
|
1265
1422
|
stage,
|
|
1266
1423
|
label: forensicStageLabel(stage),
|
|
1424
|
+
stageDone: stageTotal > 0 ? Math.min(safeCount(progress.stageDone), stageTotal) : 0,
|
|
1425
|
+
stageTotal,
|
|
1426
|
+
overall: safeFraction(progress.overall),
|
|
1267
1427
|
elapsedMs: safeDuration(progress.elapsedMs),
|
|
1268
1428
|
stageElapsedMs: safeDuration(progress.stageElapsedMs),
|
|
1269
1429
|
updatedAt: safeDuration(progress.updatedAt) || Date.now(),
|
|
@@ -1289,6 +1449,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1289
1449
|
const requiresReportConsent = opts.requireReportConsent === true && !isLoginFlow;
|
|
1290
1450
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
1291
1451
|
const onToken = deferred();
|
|
1452
|
+
const setupExit = deferred();
|
|
1292
1453
|
const decision = deferred();
|
|
1293
1454
|
const migrateRequest = deferred();
|
|
1294
1455
|
let stats = null;
|
|
@@ -1296,6 +1457,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1296
1457
|
let switchAccountUrl = "";
|
|
1297
1458
|
let connected = Boolean(opts.initialToken?.token);
|
|
1298
1459
|
let activeDeviceToken = opts.initialToken?.token || "";
|
|
1460
|
+
let activeAccountEmail = "";
|
|
1299
1461
|
let pendingLocalAuth = null;
|
|
1300
1462
|
let reportConsentGranted = !requiresReportConsent;
|
|
1301
1463
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
@@ -1330,8 +1492,8 @@ export function startCallbackServer(opts = {}) {
|
|
|
1330
1492
|
return false;
|
|
1331
1493
|
return revokeDeviceToken(pending.token);
|
|
1332
1494
|
};
|
|
1333
|
-
const completeDeviceLogin = async (token) => {
|
|
1334
|
-
await authedAxios(token).post("/api/extension/mcp/local-auth/complete-device", {}, { timeout: 10_000 });
|
|
1495
|
+
const completeDeviceLogin = async (token, encryptionPreference = "maximum") => {
|
|
1496
|
+
await authedAxios(token).post("/api/extension/mcp/local-auth/complete-device", { encryptionPreference }, { timeout: 10_000 });
|
|
1335
1497
|
};
|
|
1336
1498
|
const close = () => {
|
|
1337
1499
|
if (timer)
|
|
@@ -1407,6 +1569,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1407
1569
|
const firstToken = !onToken.settled();
|
|
1408
1570
|
connected = true;
|
|
1409
1571
|
activeDeviceToken = token;
|
|
1572
|
+
activeAccountEmail = pendingLocalAuth?.email || activeAccountEmail;
|
|
1410
1573
|
pendingLocalAuth = null;
|
|
1411
1574
|
const callbackToken = { token, key };
|
|
1412
1575
|
if (firstToken) {
|
|
@@ -1425,6 +1588,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1425
1588
|
"/stats",
|
|
1426
1589
|
"/billing-status",
|
|
1427
1590
|
"/billing-checkout",
|
|
1591
|
+
"/billing-checkout-sync",
|
|
1428
1592
|
"/billing-portal",
|
|
1429
1593
|
"/progress",
|
|
1430
1594
|
"/migrate",
|
|
@@ -1563,6 +1727,38 @@ export function startCallbackServer(opts = {}) {
|
|
|
1563
1727
|
json(res, detail.status, { ok: false, error: detail.message });
|
|
1564
1728
|
}
|
|
1565
1729
|
};
|
|
1730
|
+
const handleLocalSkipEncryption = async (res, body) => {
|
|
1731
|
+
if (rejectIfLocalAuthBlocked(res, asString(body.nonce)))
|
|
1732
|
+
return;
|
|
1733
|
+
if (!pendingLocalAuth || pendingLocalAuth.expiresAtMs <= Date.now()) {
|
|
1734
|
+
await revokePendingLocalAuth();
|
|
1735
|
+
return void json(res, 409, {
|
|
1736
|
+
ok: false,
|
|
1737
|
+
error: "This local login expired. Send a new verification code.",
|
|
1738
|
+
reset: true,
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
if (pendingLocalAuth.mode !== "setup") {
|
|
1742
|
+
return void json(res, 409, {
|
|
1743
|
+
ok: false,
|
|
1744
|
+
error: "This account already uses encrypted memory. Enter the vault passphrase to continue.",
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
await completeDeviceLogin(pendingLocalAuth.token, "standard");
|
|
1749
|
+
resolveLocalToken(pendingLocalAuth.token, undefined);
|
|
1750
|
+
json(res, 200, {
|
|
1751
|
+
ok: true,
|
|
1752
|
+
connected: true,
|
|
1753
|
+
encryptionEnabled: false,
|
|
1754
|
+
encryptionPreference: "standard",
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
catch (error) {
|
|
1758
|
+
const detail = publicAxiosError(error, "Failed to continue without encryption");
|
|
1759
|
+
json(res, detail.status, { ok: false, error: detail.message });
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1566
1762
|
server = http.createServer((req, res) => {
|
|
1567
1763
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1568
1764
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -1627,6 +1823,18 @@ export function startCallbackServer(opts = {}) {
|
|
|
1627
1823
|
});
|
|
1628
1824
|
return;
|
|
1629
1825
|
}
|
|
1826
|
+
if (route === "/local-auth/skip-encryption" && req.method === "POST") {
|
|
1827
|
+
let body;
|
|
1828
|
+
try {
|
|
1829
|
+
body = await readJsonBody(req);
|
|
1830
|
+
}
|
|
1831
|
+
catch {
|
|
1832
|
+
text(res, 400, "bad json");
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
await handleLocalSkipEncryption(res, body);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1630
1838
|
if (route === "/launch-agent" && req.method === "POST") {
|
|
1631
1839
|
let body;
|
|
1632
1840
|
try {
|
|
@@ -1740,7 +1948,18 @@ export function startCallbackServer(opts = {}) {
|
|
|
1740
1948
|
const token = activeDeviceToken || new KeyStore().getToken();
|
|
1741
1949
|
const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
|
|
1742
1950
|
if (!token) {
|
|
1743
|
-
json(res, 200, {
|
|
1951
|
+
json(res, 200, {
|
|
1952
|
+
plan: "free",
|
|
1953
|
+
paid: false,
|
|
1954
|
+
trialAvailable: true,
|
|
1955
|
+
trialUsed: false,
|
|
1956
|
+
subscriptionStatus: null,
|
|
1957
|
+
trialStartedAt: null,
|
|
1958
|
+
trialEndedAt: null,
|
|
1959
|
+
currentPeriodEnd: null,
|
|
1960
|
+
cancelAtPeriodEnd: false,
|
|
1961
|
+
pricingUrl,
|
|
1962
|
+
});
|
|
1744
1963
|
return;
|
|
1745
1964
|
}
|
|
1746
1965
|
try {
|
|
@@ -1752,24 +1971,84 @@ export function startCallbackServer(opts = {}) {
|
|
|
1752
1971
|
const plan = (asString(response.data?.plan) || "free").toLowerCase();
|
|
1753
1972
|
const trialUsed = response.data?.billing?.trialUsed === true;
|
|
1754
1973
|
const trialAvailable = response.data?.billing?.trialAvailable !== false;
|
|
1974
|
+
const accountEmail = asString(profileResponse?.data?.email) || activeAccountEmail;
|
|
1975
|
+
if (accountEmail)
|
|
1976
|
+
activeAccountEmail = accountEmail;
|
|
1755
1977
|
json(res, 200, {
|
|
1756
1978
|
plan,
|
|
1757
1979
|
paid: ["pro", "power", "team", "enterprise"].includes(plan),
|
|
1758
1980
|
trialAvailable,
|
|
1759
1981
|
trialUsed,
|
|
1982
|
+
subscriptionStatus: asString(response.data?.billing?.subscriptionStatus) || null,
|
|
1983
|
+
trialStartedAt: asString(response.data?.billing?.trialStartedAt) || null,
|
|
1984
|
+
trialEndedAt: asString(response.data?.billing?.trialEndedAt) || null,
|
|
1985
|
+
currentPeriodEnd: asString(response.data?.billing?.currentPeriodEnd) || null,
|
|
1986
|
+
cancelAtPeriodEnd: response.data?.billing?.cancelAtPeriodEnd === true,
|
|
1760
1987
|
historicalConversationQuota: response.data?.historicalConversationQuota ?? null,
|
|
1761
1988
|
memoryProcessingQuota: response.data?.memoryProcessingQuota ?? null,
|
|
1762
1989
|
memorySearchQuota: response.data?.memorySearchQuota ?? null,
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
email:
|
|
1766
|
-
avatarUrl: asString(profileResponse
|
|
1990
|
+
activation: response.data?.activation ?? null,
|
|
1991
|
+
account: accountEmail ? {
|
|
1992
|
+
email: accountEmail,
|
|
1993
|
+
avatarUrl: asString(profileResponse?.data?.avatarUrl) || "",
|
|
1767
1994
|
} : null,
|
|
1768
1995
|
pricingUrl,
|
|
1769
1996
|
});
|
|
1770
1997
|
}
|
|
1771
1998
|
catch {
|
|
1772
|
-
json(res, 200, {
|
|
1999
|
+
json(res, 200, {
|
|
2000
|
+
plan: "unknown",
|
|
2001
|
+
paid: false,
|
|
2002
|
+
trialAvailable: false,
|
|
2003
|
+
trialUsed: false,
|
|
2004
|
+
subscriptionStatus: null,
|
|
2005
|
+
trialStartedAt: null,
|
|
2006
|
+
trialEndedAt: null,
|
|
2007
|
+
currentPeriodEnd: null,
|
|
2008
|
+
cancelAtPeriodEnd: false,
|
|
2009
|
+
pricingUrl,
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
if (route === "/billing-checkout-sync" && req.method === "POST") {
|
|
2015
|
+
let body;
|
|
2016
|
+
try {
|
|
2017
|
+
body = await readJsonBody(req);
|
|
2018
|
+
}
|
|
2019
|
+
catch {
|
|
2020
|
+
text(res, 400, "bad json");
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (!checkNonce(asString(body.nonce)))
|
|
2024
|
+
return void text(res, 403, "bad nonce");
|
|
2025
|
+
const sessionId = asString(body.sessionId);
|
|
2026
|
+
if (!sessionId || !/^cs_(?:test_|live_)?[A-Za-z0-9_]+$/.test(sessionId)) {
|
|
2027
|
+
return void json(res, 400, {
|
|
2028
|
+
error: "INVALID_CHECKOUT_SESSION",
|
|
2029
|
+
message: "Checkout session is missing or invalid.",
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
const token = activeDeviceToken || new KeyStore().getToken();
|
|
2033
|
+
if (!connected || !token) {
|
|
2034
|
+
return void json(res, 401, {
|
|
2035
|
+
error: "ECHOMEM_LOGIN_REQUIRED",
|
|
2036
|
+
message: "Connect your EchoMem account before syncing checkout.",
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
try {
|
|
2040
|
+
const response = await axios.post(hostedBillingEndpoint("/api/billing/checkout/sync"), { sessionId }, {
|
|
2041
|
+
timeout: 15_000,
|
|
2042
|
+
headers: {
|
|
2043
|
+
"Content-Type": "application/json",
|
|
2044
|
+
Authorization: `Bearer ${token}`,
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
json(res, 200, { ok: response.data?.ok === true });
|
|
2048
|
+
}
|
|
2049
|
+
catch (error) {
|
|
2050
|
+
const detail = publicAxiosError(error, "Checkout is still syncing.");
|
|
2051
|
+
json(res, detail.status, { error: "BILLING_SYNC_PENDING", message: detail.message });
|
|
1773
2052
|
}
|
|
1774
2053
|
return;
|
|
1775
2054
|
}
|
|
@@ -1837,7 +2116,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1837
2116
|
message: "Echo billing returned an unexpected destination.",
|
|
1838
2117
|
});
|
|
1839
2118
|
}
|
|
1840
|
-
json(res, 200, {
|
|
2119
|
+
json(res, 200, {
|
|
2120
|
+
url: hostedUrl,
|
|
2121
|
+
...(isCheckout && typeof response.data?.sessionId === "string"
|
|
2122
|
+
? { sessionId: response.data.sessionId }
|
|
2123
|
+
: {}),
|
|
2124
|
+
});
|
|
1841
2125
|
}
|
|
1842
2126
|
catch (error) {
|
|
1843
2127
|
const detail = publicAxiosError(error, isCheckout ? "Could not start secure checkout." : "Could not open secure plan management.");
|
|
@@ -1931,6 +2215,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1931
2215
|
}
|
|
1932
2216
|
const validation = validateForensicReportForSetup(payload);
|
|
1933
2217
|
if (!validation.ok) {
|
|
2218
|
+
console.error(`[echomem] local report validation failed: ${validation.code} — ${validation.message}`);
|
|
1934
2219
|
return void json(res, 500, {
|
|
1935
2220
|
schemaVersion: 1,
|
|
1936
2221
|
kind: "failed",
|
|
@@ -2000,22 +2285,25 @@ export function startCallbackServer(opts = {}) {
|
|
|
2000
2285
|
}
|
|
2001
2286
|
connected = false;
|
|
2002
2287
|
activeDeviceToken = "";
|
|
2288
|
+
activeAccountEmail = "";
|
|
2003
2289
|
const revokedPendingCredential = await revokePendingLocalAuth();
|
|
2004
2290
|
stats = null;
|
|
2005
2291
|
migrateStarted = false;
|
|
2006
2292
|
progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
2293
|
+
try {
|
|
2294
|
+
await logoutHandler?.();
|
|
2295
|
+
}
|
|
2296
|
+
catch (e) {
|
|
2297
|
+
console.error(`Could not reset local login state: ${e instanceof Error ? e.message : String(e)}`);
|
|
2298
|
+
}
|
|
2007
2299
|
json(res, 200, {
|
|
2008
2300
|
ok: true,
|
|
2301
|
+
connected: false,
|
|
2009
2302
|
authUrl,
|
|
2010
2303
|
switchAccountUrl: switchAccountUrl || authUrl,
|
|
2011
2304
|
localOnly: true,
|
|
2012
2305
|
revokedPendingCredential,
|
|
2013
2306
|
});
|
|
2014
|
-
Promise.resolve()
|
|
2015
|
-
.then(() => logoutHandler?.())
|
|
2016
|
-
.catch((e) => {
|
|
2017
|
-
console.error(`Could not reset local login state: ${e instanceof Error ? e.message : String(e)}`);
|
|
2018
|
-
});
|
|
2019
2307
|
return;
|
|
2020
2308
|
}
|
|
2021
2309
|
if (route === "/migrate" && req.method === "POST") {
|
|
@@ -2075,6 +2363,9 @@ export function startCallbackServer(opts = {}) {
|
|
|
2075
2363
|
}
|
|
2076
2364
|
}
|
|
2077
2365
|
json(res, 200, { ok: true });
|
|
2366
|
+
if (!onToken.settled()) {
|
|
2367
|
+
setupExit.resolve("skip");
|
|
2368
|
+
}
|
|
2078
2369
|
decision.resolve("skip");
|
|
2079
2370
|
close();
|
|
2080
2371
|
return;
|
|
@@ -2101,6 +2392,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
2101
2392
|
port,
|
|
2102
2393
|
onToken: onToken.promise,
|
|
2103
2394
|
wait: onToken.promise,
|
|
2395
|
+
setupExit: setupExit.promise,
|
|
2104
2396
|
decision: decision.promise,
|
|
2105
2397
|
migrateRequest: migrateRequest.promise,
|
|
2106
2398
|
setTokenRefreshHandler: (handler) => {
|
|
@@ -2311,6 +2603,9 @@ async function cmdSetup(flags) {
|
|
|
2311
2603
|
if (!flags["no-codex-skills"]) {
|
|
2312
2604
|
writeCodexSkillsForTargets(targets);
|
|
2313
2605
|
}
|
|
2606
|
+
if (flags["install-save-hooks"]) {
|
|
2607
|
+
writeSaveCheckpointHooksForTargets(targets);
|
|
2608
|
+
}
|
|
2314
2609
|
console.log("");
|
|
2315
2610
|
if (flags["skip-login"] || flags["no-login"]) {
|
|
2316
2611
|
// init drives login itself right after, so the "skipped" note would be misleading there.
|
|
@@ -2333,7 +2628,14 @@ async function cmdSetup(flags) {
|
|
|
2333
2628
|
async function cmdInit(flags) {
|
|
2334
2629
|
console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
|
|
2335
2630
|
// 1. Configure every installed agent + write AGENTS.md. Hold login + HUD so we control ordering.
|
|
2336
|
-
await cmdSetup({
|
|
2631
|
+
await cmdSetup({
|
|
2632
|
+
...flags,
|
|
2633
|
+
all: true,
|
|
2634
|
+
"skip-login": true,
|
|
2635
|
+
"with-hud": false,
|
|
2636
|
+
"init-quiet": true,
|
|
2637
|
+
"install-save-hooks": flags["no-save-hooks"] !== true,
|
|
2638
|
+
});
|
|
2337
2639
|
// 2. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
|
|
2338
2640
|
if (!flags["no-hud"])
|
|
2339
2641
|
await cmdSetupHud(flags);
|
|
@@ -2400,6 +2702,23 @@ function writeCodexSkillsForTargets(targets) {
|
|
|
2400
2702
|
console.log(`ℹ️ Could not install EchoMem Codex skills: ${error instanceof Error ? error.message : String(error)}`);
|
|
2401
2703
|
}
|
|
2402
2704
|
}
|
|
2705
|
+
function writeSaveCheckpointHooksForTargets(targets) {
|
|
2706
|
+
const clients = new Set();
|
|
2707
|
+
if (targets.some((target) => target.id === "codex"))
|
|
2708
|
+
clients.add("codex");
|
|
2709
|
+
if (targets.some((target) => target.id === "claude-code"))
|
|
2710
|
+
clients.add("claude-code");
|
|
2711
|
+
if (clients.size === 0) {
|
|
2712
|
+
console.log("ℹ️ No hook-capable Codex or Claude Code client was detected; private-save checkpoint hooks were not installed.");
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
const mode = clients.size === 2 ? "both" : [...clients][0];
|
|
2716
|
+
const paths = installSaveCheckpointHooks(mode);
|
|
2717
|
+
console.log(`✅ Installed EchoMem private-save checkpoint hooks:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
|
|
2718
|
+
if (clients.has("codex")) {
|
|
2719
|
+
console.log(" Codex: start a new session and run /hooks once to review and trust the hook.");
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2403
2722
|
async function cmdUpdate(flags) {
|
|
2404
2723
|
if (!flags.dev)
|
|
2405
2724
|
installDurableGlobalUpdate();
|
|
@@ -2544,12 +2863,16 @@ async function cmdOnboarding(flags) {
|
|
|
2544
2863
|
const forensicStartedAt = Date.now();
|
|
2545
2864
|
let forensicStageStartedAt = forensicStartedAt;
|
|
2546
2865
|
let forensicStage = "starting";
|
|
2866
|
+
let forensicOverall = 0;
|
|
2547
2867
|
let forensicProgress = {
|
|
2548
2868
|
status: "running",
|
|
2549
2869
|
scanned: 0,
|
|
2550
2870
|
total: 0,
|
|
2551
2871
|
stage: forensicStage,
|
|
2552
2872
|
label: forensicStageLabel(forensicStage),
|
|
2873
|
+
stageDone: 0,
|
|
2874
|
+
stageTotal: 0,
|
|
2875
|
+
overall: 0,
|
|
2553
2876
|
elapsedMs: 0,
|
|
2554
2877
|
stageElapsedMs: 0,
|
|
2555
2878
|
updatedAt: forensicStartedAt,
|
|
@@ -2562,7 +2885,18 @@ async function cmdOnboarding(flags) {
|
|
|
2562
2885
|
requireReportConsent: true,
|
|
2563
2886
|
getStats: () => stats,
|
|
2564
2887
|
getReport: () => forensicReport,
|
|
2565
|
-
getReportProgress: () =>
|
|
2888
|
+
getReportProgress: () => {
|
|
2889
|
+
if (forensicProgress.status !== "running")
|
|
2890
|
+
return forensicProgress;
|
|
2891
|
+
const now = Date.now();
|
|
2892
|
+
const sinceWorkerUpdate = Math.max(0, now - forensicProgress.updatedAt);
|
|
2893
|
+
return {
|
|
2894
|
+
...forensicProgress,
|
|
2895
|
+
elapsedMs: forensicProgress.elapsedMs + sinceWorkerUpdate,
|
|
2896
|
+
stageElapsedMs: forensicProgress.stageElapsedMs + sinceWorkerUpdate,
|
|
2897
|
+
updatedAt: now,
|
|
2898
|
+
};
|
|
2899
|
+
},
|
|
2566
2900
|
onReportConsent: (allowed) => {
|
|
2567
2901
|
if (!allowed) {
|
|
2568
2902
|
forensicConsent = "declined";
|
|
@@ -2572,6 +2906,9 @@ async function cmdOnboarding(flags) {
|
|
|
2572
2906
|
total: 0,
|
|
2573
2907
|
stage: "failed",
|
|
2574
2908
|
label: "Local scan skipped",
|
|
2909
|
+
stageDone: 0,
|
|
2910
|
+
stageTotal: 0,
|
|
2911
|
+
overall: 0,
|
|
2575
2912
|
elapsedMs: Date.now() - forensicStartedAt,
|
|
2576
2913
|
stageElapsedMs: Date.now() - forensicStageStartedAt,
|
|
2577
2914
|
updatedAt: Date.now(),
|
|
@@ -2586,12 +2923,16 @@ async function cmdOnboarding(flags) {
|
|
|
2586
2923
|
const now = Date.now();
|
|
2587
2924
|
forensicStage = "starting";
|
|
2588
2925
|
forensicStageStartedAt = now;
|
|
2926
|
+
forensicOverall = 0;
|
|
2589
2927
|
forensicProgress = {
|
|
2590
2928
|
status: "running",
|
|
2591
2929
|
scanned: 0,
|
|
2592
2930
|
total: 0,
|
|
2593
2931
|
stage: forensicStage,
|
|
2594
2932
|
label: forensicStageLabel(forensicStage),
|
|
2933
|
+
stageDone: 0,
|
|
2934
|
+
stageTotal: 0,
|
|
2935
|
+
overall: 0,
|
|
2595
2936
|
elapsedMs: now - forensicStartedAt,
|
|
2596
2937
|
stageElapsedMs: 0,
|
|
2597
2938
|
updatedAt: now,
|
|
@@ -2616,6 +2957,8 @@ async function cmdOnboarding(flags) {
|
|
|
2616
2957
|
forensicStageStartedAt = now;
|
|
2617
2958
|
console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
|
|
2618
2959
|
}
|
|
2960
|
+
// Latched, so a caller that ever reports a smaller fraction cannot walk the bar backwards.
|
|
2961
|
+
forensicOverall = Math.max(forensicOverall, typeof progress.overall === "number" && Number.isFinite(progress.overall) ? progress.overall : 0);
|
|
2619
2962
|
forensicProgress = {
|
|
2620
2963
|
status: "running",
|
|
2621
2964
|
scanned: progress.done,
|
|
@@ -2623,6 +2966,13 @@ async function cmdOnboarding(flags) {
|
|
|
2623
2966
|
stage: forensicStage,
|
|
2624
2967
|
label: forensicStageLabel(forensicStage),
|
|
2625
2968
|
detail: progress.detail,
|
|
2969
|
+
stageDone: typeof progress.stageDone === "number" && Number.isFinite(progress.stageDone)
|
|
2970
|
+
? Math.max(0, Math.floor(progress.stageDone))
|
|
2971
|
+
: 0,
|
|
2972
|
+
stageTotal: typeof progress.stageTotal === "number" && Number.isFinite(progress.stageTotal)
|
|
2973
|
+
? Math.max(0, Math.floor(progress.stageTotal))
|
|
2974
|
+
: 0,
|
|
2975
|
+
overall: forensicOverall,
|
|
2626
2976
|
elapsedMs: now - forensicStartedAt,
|
|
2627
2977
|
stageElapsedMs: now - forensicStageStartedAt,
|
|
2628
2978
|
updatedAt: now,
|
|
@@ -2639,6 +2989,9 @@ async function cmdOnboarding(flags) {
|
|
|
2639
2989
|
total: forensicProgress.total,
|
|
2640
2990
|
stage: "failed",
|
|
2641
2991
|
label: "Local scan failed",
|
|
2992
|
+
stageDone: forensicProgress.stageDone,
|
|
2993
|
+
stageTotal: forensicProgress.stageTotal,
|
|
2994
|
+
overall: forensicOverall,
|
|
2642
2995
|
elapsedMs: now - forensicStartedAt,
|
|
2643
2996
|
stageElapsedMs: now - forensicStageStartedAt,
|
|
2644
2997
|
updatedAt: now,
|
|
@@ -2650,7 +3003,16 @@ async function cmdOnboarding(flags) {
|
|
|
2650
3003
|
let token;
|
|
2651
3004
|
let key;
|
|
2652
3005
|
try {
|
|
2653
|
-
|
|
3006
|
+
const authResult = await Promise.race([
|
|
3007
|
+
srv.onToken.then((credentials) => ({ kind: "token", credentials })),
|
|
3008
|
+
srv.setupExit.then(() => ({ kind: "skip" })),
|
|
3009
|
+
]);
|
|
3010
|
+
if (authResult.kind === "skip") {
|
|
3011
|
+
srv.close();
|
|
3012
|
+
console.log("Vault setup deferred. Your local coding history is unchanged; run `echomem-mcp init` when you are ready to continue.");
|
|
3013
|
+
return false;
|
|
3014
|
+
}
|
|
3015
|
+
({ token, key } = authResult.credentials);
|
|
2654
3016
|
}
|
|
2655
3017
|
catch (e) {
|
|
2656
3018
|
srv.close();
|
|
@@ -2667,13 +3029,22 @@ async function cmdOnboarding(flags) {
|
|
|
2667
3029
|
let exactDiscovery = Promise.resolve(null);
|
|
2668
3030
|
let refreshGeneration = 0;
|
|
2669
3031
|
let latestPendingEstimate = 0;
|
|
3032
|
+
// Local discovery is account-independent. Keep one snapshot for this onboarding process and
|
|
3033
|
+
// overlay each connected account's processed keys instead of rereading every JSONL on switch.
|
|
3034
|
+
const localDiscovery = createLocalDiscoveryCache({
|
|
3035
|
+
loadExact: async () => {
|
|
3036
|
+
// Publish the quick metadata result before starting the heavier exact worker.
|
|
3037
|
+
await delay(250);
|
|
3038
|
+
return discoverMigratableSessionsOffThread();
|
|
3039
|
+
},
|
|
3040
|
+
});
|
|
2670
3041
|
// The account's already-imported keys, shared so the /migrate sizing can assemble ONLY pending sessions.
|
|
2671
3042
|
let lastProcessedImportKeys = null;
|
|
2672
3043
|
const resetLocalLoginState = () => {
|
|
2673
3044
|
refreshGeneration++;
|
|
2674
3045
|
stats = null;
|
|
2675
3046
|
disc = null;
|
|
2676
|
-
exactDiscovery = Promise.resolve(
|
|
3047
|
+
exactDiscovery = localDiscovery.pendingExact() ?? Promise.resolve(localDiscovery.peekExact());
|
|
2677
3048
|
latestPendingEstimate = 0;
|
|
2678
3049
|
lastProcessedImportKeys = null;
|
|
2679
3050
|
srv.setStats(null);
|
|
@@ -2691,7 +3062,9 @@ async function cmdOnboarding(flags) {
|
|
|
2691
3062
|
const generation = ++refreshGeneration;
|
|
2692
3063
|
lastProcessedImportKeys = null; // shared with /migrate so it can assemble only the pending sessions
|
|
2693
3064
|
let importStatusUnavailable = false;
|
|
2694
|
-
const quickDiscovery =
|
|
3065
|
+
const quickDiscovery = await localDiscovery.getQuick();
|
|
3066
|
+
if (generation !== refreshGeneration)
|
|
3067
|
+
return;
|
|
2695
3068
|
const quick = summarizeFastMigratableDiscovery(quickDiscovery);
|
|
2696
3069
|
let migratable = migratableFromFastSummary(quick);
|
|
2697
3070
|
latestPendingEstimate = migratable.pending;
|
|
@@ -2799,23 +3172,7 @@ async function cmdOnboarding(flags) {
|
|
|
2799
3172
|
console.error(`Could not check this EchoMem account's import status quickly: ${e instanceof Error ? e.message : String(e)}`);
|
|
2800
3173
|
}
|
|
2801
3174
|
});
|
|
2802
|
-
exactDiscovery =
|
|
2803
|
-
const timer = setTimeout(() => {
|
|
2804
|
-
void (async () => {
|
|
2805
|
-
try {
|
|
2806
|
-
// Start the local sizing pass without waiting on cloud/account status. The
|
|
2807
|
-
// account check is useful for tighter counts, but extraction can safely start
|
|
2808
|
-
// from local candidates because the import path skips true duplicates.
|
|
2809
|
-
await delay(250);
|
|
2810
|
-
resolve(await discoverMigratableSessionsOffThread());
|
|
2811
|
-
}
|
|
2812
|
-
catch (e) {
|
|
2813
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
2814
|
-
}
|
|
2815
|
-
})();
|
|
2816
|
-
}, 250);
|
|
2817
|
-
timer.unref?.();
|
|
2818
|
-
}).then(async (exact) => {
|
|
3175
|
+
exactDiscovery = localDiscovery.getExact().then(async (exact) => {
|
|
2819
3176
|
if (generation !== refreshGeneration)
|
|
2820
3177
|
return disc;
|
|
2821
3178
|
const initialExact = lastProcessedImportKeys
|
|
@@ -2897,7 +3254,7 @@ async function cmdOnboarding(flags) {
|
|
|
2897
3254
|
failed: 0,
|
|
2898
3255
|
extracted: 0,
|
|
2899
3256
|
});
|
|
2900
|
-
const fullPayload = withCandidateSessions(await
|
|
3257
|
+
const fullPayload = withCandidateSessions(await buildCollectedStatsPayloadOffThread({
|
|
2901
3258
|
sessions: sessionSummary,
|
|
2902
3259
|
migratable,
|
|
2903
3260
|
discovery: { phase: "full", exact: true },
|
|
@@ -3380,7 +3737,8 @@ Manual / headless:
|
|
|
3380
3737
|
${MCP_UPDATE_ALL_COMMAND} # one-shot latest update for detected clients, no browser login
|
|
3381
3738
|
${MCP_UPDATE_COMMAND} --client codex # update one client only
|
|
3382
3739
|
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]
|
|
3740
|
+
echomem-mcp setup --with-hud --install-hud-hooks --install-save-hooks --client codex [--hud-client auto]
|
|
3741
|
+
echomem-mcp setup --install-save-hooks --all Install proactive private-save completion checks
|
|
3384
3742
|
echomem-mcp sync-usage --days 7 --limit 50 --dry-run
|
|
3385
3743
|
|
|
3386
3744
|
Current bridge version: ${MCP_PACKAGE_VERSION}
|