@echomem/mcp 1.4.7 → 1.4.8
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/README.md +14 -8
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/echo-ai-city-only.html +31 -13
- package/dist/city/echo-ai-city-only.template.html +31 -13
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/monitor.js +36 -68
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +633 -69
- package/dist/index.js +116 -22
- package/dist/migrate.js +18 -0
- package/dist/setup-page.js +978 -60
- package/dist/setup.js +355 -42
- package/dist/v1-contract.js +20 -2
- package/package.json +3 -2
package/dist/setup.js
CHANGED
|
@@ -129,6 +129,51 @@ export function writeCodexConfig(configPath, entry) {
|
|
|
129
129
|
fs.appendFileSync(configPath, sep + block + "\n");
|
|
130
130
|
return "wrote";
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Write the EchoMem guidance block into the agent's GLOBAL memory file (~/.codex/AGENTS.md,
|
|
134
|
+
* ~/.claude/CLAUDE.md) so the agent treats EchoMem as its core memory tool without the tool
|
|
135
|
+
* descriptions having to over-explain "when to call me". Marker-fenced + idempotent: only our
|
|
136
|
+
* block is ever replaced; the user's own content is never touched. Project-level files are
|
|
137
|
+
* deliberately NOT written — those belong to the user.
|
|
138
|
+
*/
|
|
139
|
+
const AGENTS_MD_BEGIN = "<!-- echomem:begin -->";
|
|
140
|
+
const AGENTS_MD_END = "<!-- echomem:end -->";
|
|
141
|
+
function echomemGuidanceBlock() {
|
|
142
|
+
return [
|
|
143
|
+
AGENTS_MD_BEGIN,
|
|
144
|
+
"## Memory (EchoMem)",
|
|
145
|
+
"EchoMem is your long-term memory across all coding sessions and tools.",
|
|
146
|
+
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
147
|
+
"- When meaningful work wraps up (a decision, a fix, a milestone) or the user asks to remember something: call `save_conversation`.",
|
|
148
|
+
"- 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.",
|
|
149
|
+
"- If the user asks to show, reopen, restart, or bring back the EchoMem HUD (the context-health overlay), run the shell command `echomem-hud app --client auto`.",
|
|
150
|
+
"- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
|
|
151
|
+
AGENTS_MD_END,
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
export function writeAgentsMemoryGuidance(filePath) {
|
|
155
|
+
let content = "";
|
|
156
|
+
try {
|
|
157
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* fresh file */
|
|
161
|
+
}
|
|
162
|
+
const block = echomemGuidanceBlock();
|
|
163
|
+
const start = content.indexOf(AGENTS_MD_BEGIN);
|
|
164
|
+
const end = content.indexOf(AGENTS_MD_END);
|
|
165
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
166
|
+
if (start >= 0 && end > start) {
|
|
167
|
+
const current = content.slice(start, end + AGENTS_MD_END.length);
|
|
168
|
+
if (current === block)
|
|
169
|
+
return "exists";
|
|
170
|
+
fs.writeFileSync(filePath, content.slice(0, start) + block + content.slice(end + AGENTS_MD_END.length));
|
|
171
|
+
return "updated";
|
|
172
|
+
}
|
|
173
|
+
const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
174
|
+
fs.appendFileSync(filePath, sep + block + "\n");
|
|
175
|
+
return "wrote";
|
|
176
|
+
}
|
|
132
177
|
/** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
|
|
133
178
|
export function writeJsonClientConfig(configPath, entry) {
|
|
134
179
|
let config = {};
|
|
@@ -275,8 +320,8 @@ export function resolveServerEntryVersion(entry) {
|
|
|
275
320
|
}
|
|
276
321
|
const candidates = [...args, command].filter((value) => typeof value === "string");
|
|
277
322
|
for (const candidate of candidates) {
|
|
278
|
-
|
|
279
|
-
|
|
323
|
+
// Direct local/dev entries often look like ".../packages/mcp-server/dist/index.js";
|
|
324
|
+
// they do not contain the published package name, but walking upward still finds package.json.
|
|
280
325
|
const resolved = packageVersionFromPath(candidate);
|
|
281
326
|
if (resolved)
|
|
282
327
|
return resolved;
|
|
@@ -408,11 +453,54 @@ function openBrowser(url) {
|
|
|
408
453
|
/* headless — caller prints the URL */
|
|
409
454
|
}
|
|
410
455
|
}
|
|
456
|
+
function openClaudeDesktop() {
|
|
457
|
+
if (process.platform !== "darwin") {
|
|
458
|
+
return { ok: false, message: "Could not auto-open Claude on this system. The prompt is copied - open Claude Desktop and paste it." };
|
|
459
|
+
}
|
|
460
|
+
const installedPath = firstExisting([
|
|
461
|
+
"/Applications/Claude.app",
|
|
462
|
+
path.join(os.homedir(), "Applications", "Claude.app"),
|
|
463
|
+
]);
|
|
464
|
+
const attempts = [
|
|
465
|
+
["-b", "com.anthropic.claudefordesktop"],
|
|
466
|
+
["-a", "Claude"],
|
|
467
|
+
...(installedPath ? [[installedPath]] : []),
|
|
468
|
+
];
|
|
469
|
+
const failures = [];
|
|
470
|
+
for (const args of attempts) {
|
|
471
|
+
try {
|
|
472
|
+
execFileSync("open", args, { stdio: "pipe" });
|
|
473
|
+
return { ok: true, message: "Opened Claude Desktop. The prompt is copied - paste it into Claude." };
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
failures.push(`${args.join(" ")}: ${commandFailureMessage(error)}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
message: installedPath
|
|
482
|
+
? "Could not auto-open Claude Desktop. The prompt is copied - open Claude Desktop and paste it."
|
|
483
|
+
: "Claude Desktop was not found by macOS. The prompt is copied - open Claude manually and paste it.",
|
|
484
|
+
detail: failures.join(" | "),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function commandFailureMessage(error) {
|
|
488
|
+
const maybe = error;
|
|
489
|
+
if (Buffer.isBuffer(maybe.stderr)) {
|
|
490
|
+
const stderr = maybe.stderr.toString("utf8").trim();
|
|
491
|
+
if (stderr)
|
|
492
|
+
return stderr;
|
|
493
|
+
}
|
|
494
|
+
return error instanceof Error ? error.message : String(error);
|
|
495
|
+
}
|
|
411
496
|
function migratableFromDiscovery(disc) {
|
|
412
497
|
const eta = estimateMigrationEta(disc.pending, disc.skippedActive);
|
|
498
|
+
const pendingCodex = disc.pendingCodex ?? disc.pending.filter((s) => s.source === "codex").length;
|
|
413
499
|
return {
|
|
414
500
|
pending: disc.pending.length,
|
|
415
501
|
pendingTotal: disc.pendingTotal,
|
|
502
|
+
pendingCodex,
|
|
503
|
+
pendingClaudeCode: disc.pendingClaudeCode ?? disc.pending.length - pendingCodex,
|
|
416
504
|
alreadyMigrated: disc.alreadyMigrated,
|
|
417
505
|
skippedActive: disc.skippedActive,
|
|
418
506
|
limited: disc.limited,
|
|
@@ -438,6 +526,8 @@ function migratableFromFastSummary(summary) {
|
|
|
438
526
|
return {
|
|
439
527
|
pending: summary.pending,
|
|
440
528
|
pendingTotal: summary.pendingTotal,
|
|
529
|
+
pendingCodex: summary.pendingCodex,
|
|
530
|
+
pendingClaudeCode: summary.pendingClaudeCode,
|
|
441
531
|
alreadyMigrated: summary.alreadyMigrated,
|
|
442
532
|
skippedActive: summary.skippedActive,
|
|
443
533
|
eta: summary.eta,
|
|
@@ -557,6 +647,107 @@ function serveRepoCityAsset(reqPath, res) {
|
|
|
557
647
|
fs.createReadStream(filePath).pipe(res);
|
|
558
648
|
return true;
|
|
559
649
|
}
|
|
650
|
+
function hudAssetsRoot() {
|
|
651
|
+
return fileURLToPath(new URL("../assets/hud/", import.meta.url));
|
|
652
|
+
}
|
|
653
|
+
function serveHudAsset(reqPath, res) {
|
|
654
|
+
const root = hudAssetsRoot();
|
|
655
|
+
const rel = decodeURIComponent(reqPath.slice("/hud-assets/".length));
|
|
656
|
+
const filePath = path.resolve(root, rel);
|
|
657
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
658
|
+
if (!filePath.startsWith(rootWithSep)) {
|
|
659
|
+
res.writeHead(403).end("forbidden");
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
663
|
+
res.writeHead(404).end("not found");
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
res.writeHead(200, {
|
|
667
|
+
"Content-Type": CITY_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
|
|
668
|
+
"Cache-Control": "no-store",
|
|
669
|
+
});
|
|
670
|
+
fs.createReadStream(filePath).pipe(res);
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
function firstExisting(paths) {
|
|
674
|
+
for (const candidate of paths) {
|
|
675
|
+
if (fs.existsSync(candidate))
|
|
676
|
+
return candidate;
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
function appResourcesPath(appName) {
|
|
681
|
+
const app = firstExisting([
|
|
682
|
+
`/Applications/${appName}.app`,
|
|
683
|
+
path.join(os.homedir(), "Applications", `${appName}.app`),
|
|
684
|
+
]);
|
|
685
|
+
return app ? path.join(app, "Contents", "Resources") : null;
|
|
686
|
+
}
|
|
687
|
+
function agentIconSource(id) {
|
|
688
|
+
const resources = appResourcesPath(id === "codex" ? "Codex" : "Claude");
|
|
689
|
+
if (!resources)
|
|
690
|
+
return null;
|
|
691
|
+
if (id === "codex") {
|
|
692
|
+
return firstExisting([
|
|
693
|
+
path.join(resources, "icon.png"),
|
|
694
|
+
path.join(resources, "icon-codex-dark-color.png"),
|
|
695
|
+
path.join(resources, "icon-codex-light.png"),
|
|
696
|
+
path.join(resources, "icon.icns"),
|
|
697
|
+
path.join(resources, "app.icns"),
|
|
698
|
+
path.join(resources, "electron.icns"),
|
|
699
|
+
]);
|
|
700
|
+
}
|
|
701
|
+
return firstExisting([
|
|
702
|
+
path.join(resources, "icon.png"),
|
|
703
|
+
path.join(resources, "app.png"),
|
|
704
|
+
path.join(resources, "icon.icns"),
|
|
705
|
+
path.join(resources, "electron.icns"),
|
|
706
|
+
]);
|
|
707
|
+
}
|
|
708
|
+
function convertedAgentIconPath(id, sourcePath) {
|
|
709
|
+
if (path.extname(sourcePath).toLowerCase() === ".png")
|
|
710
|
+
return sourcePath;
|
|
711
|
+
if (process.platform !== "darwin" || path.extname(sourcePath).toLowerCase() !== ".icns")
|
|
712
|
+
return null;
|
|
713
|
+
const stat = fs.statSync(sourcePath);
|
|
714
|
+
const cacheDir = path.join(os.homedir(), ".echomem", "cache", "agent-icons");
|
|
715
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
716
|
+
const out = path.join(cacheDir, `${id}-${Math.round(stat.mtimeMs)}-${stat.size}.png`);
|
|
717
|
+
if (fs.existsSync(out))
|
|
718
|
+
return out;
|
|
719
|
+
try {
|
|
720
|
+
execFileSync("sips", ["-s", "format", "png", sourcePath, "--out", out], { stdio: "ignore" });
|
|
721
|
+
return fs.existsSync(out) ? out : null;
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
try {
|
|
725
|
+
fs.rmSync(out, { force: true });
|
|
726
|
+
}
|
|
727
|
+
catch { }
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function serveAgentIcon(reqPath, res) {
|
|
732
|
+
const raw = decodeURIComponent(reqPath.slice("/agent-icons/".length)).replace(/\.png$/i, "");
|
|
733
|
+
const id = raw === "codex" || raw === "claude-desktop" ? raw : null;
|
|
734
|
+
if (!id) {
|
|
735
|
+
res.writeHead(404).end("not found");
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
const source = agentIconSource(id);
|
|
739
|
+
const filePath = source ? convertedAgentIconPath(id, source) : null;
|
|
740
|
+
if (!filePath) {
|
|
741
|
+
res.writeHead(404).end("not found");
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
res.writeHead(200, {
|
|
745
|
+
"Content-Type": "image/png",
|
|
746
|
+
"Cache-Control": "no-store",
|
|
747
|
+
});
|
|
748
|
+
fs.createReadStream(filePath).pipe(res);
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
560
751
|
function discoverMigratableSessionsOffThread() {
|
|
561
752
|
const migrateUrl = new URL("./migrate.js", import.meta.url).href;
|
|
562
753
|
const code = `
|
|
@@ -739,6 +930,14 @@ export function startCallbackServer(opts = {}) {
|
|
|
739
930
|
serveRepoCityAsset(route, res);
|
|
740
931
|
return;
|
|
741
932
|
}
|
|
933
|
+
if (route.startsWith("/hud-assets/") && req.method === "GET") {
|
|
934
|
+
serveHudAsset(route, res);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (route.startsWith("/agent-icons/") && req.method === "GET") {
|
|
938
|
+
serveAgentIcon(route, res);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
742
941
|
if (route === "/setup" && req.method === "GET") {
|
|
743
942
|
res.writeHead(200, {
|
|
744
943
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -749,7 +948,25 @@ export function startCallbackServer(opts = {}) {
|
|
|
749
948
|
if (route === "/config" && req.method === "GET") {
|
|
750
949
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
751
950
|
return void text(res, 403, "bad nonce");
|
|
752
|
-
json(res, 200, { connected, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true });
|
|
951
|
+
json(res, 200, { connected, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true, workspacePath: process.cwd() });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (route === "/launch-agent" && req.method === "POST") {
|
|
955
|
+
let body;
|
|
956
|
+
try {
|
|
957
|
+
body = await readJsonBody(req);
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
text(res, 400, "bad json");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (!checkNonce(asString(body.nonce)))
|
|
964
|
+
return void text(res, 403, "bad nonce");
|
|
965
|
+
const agent = asString(body.agent);
|
|
966
|
+
if (agent !== "claude-desktop")
|
|
967
|
+
return void json(res, 400, { ok: false, message: "Unsupported agent." });
|
|
968
|
+
const result = openClaudeDesktop();
|
|
969
|
+
json(res, result.ok ? 200 : 501, result);
|
|
753
970
|
return;
|
|
754
971
|
}
|
|
755
972
|
if (route === "/callback" && req.method === "GET") {
|
|
@@ -1052,9 +1269,14 @@ async function cmdSetup(flags) {
|
|
|
1052
1269
|
}
|
|
1053
1270
|
}
|
|
1054
1271
|
}
|
|
1272
|
+
if (!flags["no-agents-md"]) {
|
|
1273
|
+
writeMemoryGuidanceForTargets(targets);
|
|
1274
|
+
}
|
|
1055
1275
|
console.log("");
|
|
1056
1276
|
if (flags["skip-login"] || flags["no-login"]) {
|
|
1057
|
-
|
|
1277
|
+
// init drives login itself right after, so the "skipped" note would be misleading there.
|
|
1278
|
+
if (!flags["init-quiet"])
|
|
1279
|
+
console.log(`Skipped login; existing EchoMem credentials are unchanged. Current bridge: ${MCP_PACKAGE_LABEL}`);
|
|
1058
1280
|
}
|
|
1059
1281
|
else {
|
|
1060
1282
|
await cmdLogin(flags);
|
|
@@ -1062,6 +1284,63 @@ async function cmdSetup(flags) {
|
|
|
1062
1284
|
if (flags["with-hud"])
|
|
1063
1285
|
await cmdSetupHud(flags);
|
|
1064
1286
|
}
|
|
1287
|
+
/**
|
|
1288
|
+
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
1289
|
+
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), writes the AGENTS.md
|
|
1290
|
+
* memory guidance, logs in via the browser, and launches the context HUD — the whole product in a
|
|
1291
|
+
* single command. `setup`/`update` remain the granular primitives; init just picks the "do everything"
|
|
1292
|
+
* defaults and frames the result.
|
|
1293
|
+
*/
|
|
1294
|
+
async function cmdInit(flags) {
|
|
1295
|
+
console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
|
|
1296
|
+
// 1. Configure every installed agent + write AGENTS.md. Hold login + HUD so we control ordering.
|
|
1297
|
+
await cmdSetup({ ...flags, all: true, "skip-login": true, "with-hud": false, "init-quiet": true });
|
|
1298
|
+
// 2. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
|
|
1299
|
+
if (!flags["no-hud"])
|
|
1300
|
+
await cmdSetupHud(flags);
|
|
1301
|
+
// 3. Start onboarding — opens the browser dashboard (scan → connect → extraction) and waits there.
|
|
1302
|
+
console.log("");
|
|
1303
|
+
if (!flags["skip-login"] && !flags["no-login"])
|
|
1304
|
+
await cmdLogin(flags);
|
|
1305
|
+
console.log("");
|
|
1306
|
+
console.log("🎉 EchoMem is ready.");
|
|
1307
|
+
console.log(" • MCP memory is configured for every coding agent installed on this machine.");
|
|
1308
|
+
if (!flags["no-hud"]) {
|
|
1309
|
+
console.log(' • The context HUD is running (top-right). Right-click it → "Open at login" to keep it,');
|
|
1310
|
+
console.log(' or just tell your agent "open the EchoMem HUD" anytime (it runs: echomem-hud app).');
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
console.log(' • Start the context HUD anytime with: echomem-hud app');
|
|
1314
|
+
}
|
|
1315
|
+
console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* For each configured client, add the EchoMem guidance block to its GLOBAL memory file so the
|
|
1319
|
+
* agent knows to lean on EchoMem for recall/save. Global only (~/.codex/AGENTS.md,
|
|
1320
|
+
* ~/.claude/CLAUDE.md) — project files are the user's. Skip with --no-agents-md.
|
|
1321
|
+
*/
|
|
1322
|
+
function writeMemoryGuidanceForTargets(targets) {
|
|
1323
|
+
const files = new Map(); // path → label
|
|
1324
|
+
for (const t of targets) {
|
|
1325
|
+
if (t.id === "codex")
|
|
1326
|
+
files.set(home(".codex", "AGENTS.md"), "Codex");
|
|
1327
|
+
if (t.id === "claude-code" || t.id === "claude-desktop")
|
|
1328
|
+
files.set(home(".claude", "CLAUDE.md"), "Claude");
|
|
1329
|
+
}
|
|
1330
|
+
for (const [file, label] of files) {
|
|
1331
|
+
try {
|
|
1332
|
+
const result = writeAgentsMemoryGuidance(file);
|
|
1333
|
+
if (result === "wrote")
|
|
1334
|
+
console.log(`✅ Added EchoMem memory guidance to ${label}'s global memory file: ${file}`);
|
|
1335
|
+
else if (result === "updated")
|
|
1336
|
+
console.log(`✅ Refreshed EchoMem memory guidance in ${file}`);
|
|
1337
|
+
// "exists" → silent; nothing changed.
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
console.log(`ℹ️ Could not write memory guidance to ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1065
1344
|
async function cmdUpdate(flags) {
|
|
1066
1345
|
await cmdSetup({ ...flags, "skip-login": true });
|
|
1067
1346
|
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
@@ -1544,49 +1823,79 @@ async function cmdLogin(flags) {
|
|
|
1544
1823
|
return;
|
|
1545
1824
|
}
|
|
1546
1825
|
updateProgress({ status: "starting", running: 0, queued: exact.pending.length, latest: "Creating import session." });
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
updateProgress({ status: "running", sessionId: h.sessionId, jobCount: h.jobCount, total: h.jobCount, capped: h.capped, latest: "Import session created." });
|
|
1570
|
-
sendMigrate({ sessionId: h.sessionId, jobCount: h.jobCount, ...(h.capped ? { capped: h.capped } : {}) });
|
|
1826
|
+
// Plan caps limit how many conversations one import session accepts (IMPORT_LIMIT_EXCEEDED →
|
|
1827
|
+
// startMigration slices to the cap). Instead of making the user re-run setup per batch (3000
|
|
1828
|
+
// sessions used to mean 3 clicks), loop batches automatically until everything pending is done.
|
|
1829
|
+
const onBatchProgress = (ev) => {
|
|
1830
|
+
let latestRepo;
|
|
1831
|
+
if (ev.error) {
|
|
1832
|
+
progressFailed += 1;
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
progressDone += 1;
|
|
1836
|
+
progressExtracted += ev.memories ?? 0;
|
|
1837
|
+
latestRepo = repoLabel(ev.session.cwd); // building this completed conversation belongs to
|
|
1838
|
+
}
|
|
1839
|
+
updateProgress({
|
|
1840
|
+
latest: `${ev.session.source} ${(ev.session.firstTs || "").slice(0, 10)}${ev.error ? ` failed: ${ev.error}` : ""}`,
|
|
1841
|
+
...(latestRepo ? { latestRepo } : {}),
|
|
1842
|
+
});
|
|
1843
|
+
};
|
|
1844
|
+
let remaining = exact.pending;
|
|
1845
|
+
let stoppedReason;
|
|
1846
|
+
let planLimitNote;
|
|
1847
|
+
let batchIndex = 0;
|
|
1571
1848
|
console.log("Migrating your history… keep this terminal open until it completes.");
|
|
1572
|
-
|
|
1573
|
-
|
|
1849
|
+
for (;;) {
|
|
1850
|
+
batchIndex += 1;
|
|
1851
|
+
let h;
|
|
1852
|
+
try {
|
|
1853
|
+
const controller = new AbortController();
|
|
1854
|
+
h = await withTimeout(startMigration({ pending: remaining, signal: controller.signal, onProgress: onBatchProgress }), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
|
|
1855
|
+
}
|
|
1856
|
+
catch (batchError) {
|
|
1857
|
+
if (batchIndex === 1)
|
|
1858
|
+
throw batchError; // first batch failing = the whole import failed
|
|
1859
|
+
// A later batch could not start (e.g. plan headroom exhausted). Finish gracefully with a note.
|
|
1860
|
+
planLimitNote = `Imported ${progressDone} so far — the rest hit your plan's import limit. Re-run extraction later for the remaining ${remaining.length}.`;
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
activeSessionId = h.sessionId;
|
|
1864
|
+
if (batchIndex === 1) {
|
|
1865
|
+
updateProgress({ status: "running", sessionId: h.sessionId, jobCount: activeJobCount, total: activeJobCount, latest: "Import session created." });
|
|
1866
|
+
sendMigrate({ sessionId: h.sessionId, jobCount: activeJobCount });
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
updateProgress({ latest: `Continuing automatically — batch ${batchIndex} (${remaining.length} left).` });
|
|
1870
|
+
}
|
|
1871
|
+
console.log(`Migration metrics (batch ${batchIndex}): ${h.metricsFile}`);
|
|
1872
|
+
const r = await h.done;
|
|
1873
|
+
if (r.stoppedReason) {
|
|
1874
|
+
stoppedReason = r.stoppedReason;
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
// startMigration sliced to the cap ⇒ more remain: continue with the next batch automatically.
|
|
1878
|
+
if (h.capped && remaining.length > h.jobCount) {
|
|
1879
|
+
remaining = remaining.slice(h.jobCount);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
break;
|
|
1883
|
+
}
|
|
1574
1884
|
srv.setProgress({
|
|
1575
|
-
status:
|
|
1576
|
-
sessionId:
|
|
1577
|
-
jobCount:
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
completed: r.migrated,
|
|
1885
|
+
status: stoppedReason ? "failed" : "completed",
|
|
1886
|
+
sessionId: activeSessionId || undefined,
|
|
1887
|
+
jobCount: activeJobCount,
|
|
1888
|
+
total: activeJobCount,
|
|
1889
|
+
completed: progressDone,
|
|
1581
1890
|
running: 0,
|
|
1582
1891
|
queued: 0,
|
|
1583
|
-
failed:
|
|
1584
|
-
extracted:
|
|
1585
|
-
latest:
|
|
1586
|
-
...(
|
|
1892
|
+
failed: progressFailed,
|
|
1893
|
+
extracted: progressExtracted,
|
|
1894
|
+
latest: stoppedReason ? `Stopped: ${stoppedReason}` : planLimitNote || "Import complete.",
|
|
1895
|
+
...(stoppedReason ? { error: stoppedReason } : {}),
|
|
1587
1896
|
});
|
|
1588
|
-
console.log(`Import finished: ${
|
|
1589
|
-
if (
|
|
1897
|
+
console.log(`Import finished: ${progressDone} imported, ${progressExtracted} memories, ${progressFailed} failed.`);
|
|
1898
|
+
if (stoppedReason || progressFailed)
|
|
1590
1899
|
process.exitCode = 1;
|
|
1591
1900
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1592
1901
|
srv.close();
|
|
@@ -1713,6 +2022,7 @@ function cmdLogout() {
|
|
|
1713
2022
|
const HELP = `EchoMem MCP — local memory bridge
|
|
1714
2023
|
|
|
1715
2024
|
Usage:
|
|
2025
|
+
echomem-mcp init One command: configure every installed agent + HUD + log in
|
|
1716
2026
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
1717
2027
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
|
|
1718
2028
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
@@ -1749,6 +2059,9 @@ export async function runCli(argv) {
|
|
|
1749
2059
|
const cmd = argv[0];
|
|
1750
2060
|
const flags = parseFlags(argv.slice(1));
|
|
1751
2061
|
switch (cmd) {
|
|
2062
|
+
case "init":
|
|
2063
|
+
await cmdInit(flags);
|
|
2064
|
+
return true;
|
|
1752
2065
|
case "setup":
|
|
1753
2066
|
await cmdSetup(flags);
|
|
1754
2067
|
return true;
|
package/dist/v1-contract.js
CHANGED
|
@@ -16,6 +16,7 @@ export const canonicalToolNames = {
|
|
|
16
16
|
recompose: "echo_recompose",
|
|
17
17
|
delete: "delete_memory",
|
|
18
18
|
getByContext: "get_memories_by_context",
|
|
19
|
+
checkpointByContext: "get_checkpoint_by_context",
|
|
19
20
|
};
|
|
20
21
|
export const legacyAliasToCanonical = {
|
|
21
22
|
search_memories_by_description_semantic: canonicalToolNames.search,
|
|
@@ -114,7 +115,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
114
115
|
return [
|
|
115
116
|
{
|
|
116
117
|
name: canonicalToolNames.search,
|
|
117
|
-
description: withMcpVersion(`Recall the user's prior decisions, preferences,
|
|
118
|
+
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.${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
|
|
118
119
|
inputSchema: {
|
|
119
120
|
type: "object",
|
|
120
121
|
properties: {
|
|
@@ -160,7 +161,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
160
161
|
},
|
|
161
162
|
{
|
|
162
163
|
name: canonicalToolNames.save,
|
|
163
|
-
description: "Save conversation into EchoMem
|
|
164
|
+
description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). passthrough=true stores the text verbatim as a session capsule instead — reload it later via get_memories_by_context.",
|
|
164
165
|
inputSchema: {
|
|
165
166
|
type: "object",
|
|
166
167
|
properties: {
|
|
@@ -383,6 +384,23 @@ export function listToolSpecs(opts = {}) {
|
|
|
383
384
|
required: ["contextId"],
|
|
384
385
|
},
|
|
385
386
|
},
|
|
387
|
+
{
|
|
388
|
+
name: canonicalToolNames.checkpointByContext,
|
|
389
|
+
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. 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}.`),
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: {
|
|
393
|
+
contextId: { type: "string", description: "The EchoMem contextId shown by the HUD or returned by save_conversation / Renew session." },
|
|
394
|
+
limit: { type: "number", default: 100 },
|
|
395
|
+
triggerMessage: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
|
|
398
|
+
},
|
|
399
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
400
|
+
},
|
|
401
|
+
required: ["contextId"],
|
|
402
|
+
},
|
|
403
|
+
},
|
|
386
404
|
{
|
|
387
405
|
name: canonicalToolNames.report,
|
|
388
406
|
description: "Show the user a one-screen audit of THEIR OWN AI coding usage — computed locally from their Codex/Claude Code logs ($0, nothing uploaded): how many tokens their agents spent, how much was re-reading context, reads vs memory recalls, and what changes with EchoMem. Call this when the user asks about their usage, token spend, cost, how much they're wasting, or wants a summary of their agent activity — and you may offer it once right after EchoMem is first connected. Returns formatted text to show the user verbatim. Needs no login.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.8",
|
|
4
4
|
"description": "EchoMem Cloud-First MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"start": "node dist/index.js",
|
|
21
21
|
"dev": "tsx src/index.ts",
|
|
22
22
|
"smoke": "node smoke.mjs",
|
|
23
|
-
"
|
|
23
|
+
"preview:extraction": "npm run build && node scripts/preview-extraction.mjs",
|
|
24
|
+
"test": "npm run build && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.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/hud.test.mjs",
|
|
24
25
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|