@echomem/mcp 1.4.6 → 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 +1734 -0
- package/dist/city/10-problems-report.html +649 -0
- package/dist/city/echo-ai-city-only.html +63 -653
- package/dist/city/echo-ai-city-only.template.html +35 -17
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/forensics-10-problems.js +632 -0
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +229 -22
- package/dist/hud/monitor.js +36 -68
- package/dist/hud/preload.cjs +12 -0
- package/dist/hud/server.js +413 -3
- package/dist/hud/web.js +713 -74
- package/dist/index.js +355 -28
- package/dist/migrate.js +18 -0
- package/dist/setup-page.js +978 -60
- package/dist/setup.js +353 -40
- package/dist/v1-contract.js +154 -5
- 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 = {};
|
|
@@ -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;
|