@echomem/mcp 1.4.50 → 1.4.51
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 +27 -2
- package/dist/config-files.js +63 -0
- package/dist/headless-runtime.js +59 -8
- package/dist/hud/hooks.js +85 -31
- package/dist/index.js +129 -12
- package/dist/mcp-control.js +215 -0
- package/dist/setup.js +410 -27
- package/dist/source-session.js +253 -75
- package/dist/v1-contract.js +34 -0
- package/package.json +4 -2
package/dist/setup.js
CHANGED
|
@@ -32,10 +32,13 @@ import { renderSetupPage } from "./setup-page.js";
|
|
|
32
32
|
import { parseSetupPreviewState } from "./setup-preview.js";
|
|
33
33
|
import { resolveClaudeCoworkSessionRoots, resolveClaudeDesktopSupportRoots, resolveClaudeProjectsDir, resolveCodexSessionRoots, } from "./local-data-paths.js";
|
|
34
34
|
import { isEphemeralNpxPath, reexecFromDurableRuntime, resolveDurableDistPath, resolveGlobalEntry } from "./durable-entry.js";
|
|
35
|
-
import { installSaveCheckpointHooks, installSourceSessionHooks } from "./hud/hooks.js";
|
|
35
|
+
import { installSaveCheckpointHooks, installSourceSessionHooks, removeLifecycleHooks } from "./hud/hooks.js";
|
|
36
|
+
import { atomicWriteJsonObject, atomicWriteTextFile, readJsonObjectFile } from "./config-files.js";
|
|
36
37
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
37
38
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
38
|
-
import {
|
|
39
|
+
import { startMcpControlServer } from "./mcp-control.js";
|
|
40
|
+
import { loadHudDiagnostics } from "./hud/diagnostics.js";
|
|
41
|
+
import { installHeadlessRuntimeSync, readHeadlessRuntimeInstallation, removeHeadlessRuntime, } from "./headless-runtime.js";
|
|
39
42
|
// The setup dashboard, account login, and encryption passphrase entry are all served by this
|
|
40
43
|
// localhost bridge. The hosted API only sends OTP email, verifies the code, and mints a device token.
|
|
41
44
|
const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
@@ -408,13 +411,38 @@ export function writeCodexConfig(configPath, entry, options = {}) {
|
|
|
408
411
|
if (lines.slice(start, end).join("\n").trimEnd() === block)
|
|
409
412
|
return "exists"; // already correct
|
|
410
413
|
const next = [...lines.slice(0, start), ...block.split("\n"), ...lines.slice(end)];
|
|
411
|
-
|
|
414
|
+
atomicWriteTextFile(configPath, next.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, ""));
|
|
412
415
|
return "wrote"; // replaced a stale entry → caller tells the user to restart Codex
|
|
413
416
|
}
|
|
414
417
|
const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
415
|
-
|
|
418
|
+
atomicWriteTextFile(configPath, content + sep + block + "\n");
|
|
416
419
|
return "wrote";
|
|
417
420
|
}
|
|
421
|
+
/** Remove only EchoMem's TOML section and preserve every other Codex setting. */
|
|
422
|
+
export function removeCodexConfig(configPath) {
|
|
423
|
+
let content;
|
|
424
|
+
try {
|
|
425
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error.code === "ENOENT")
|
|
429
|
+
return false;
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const lines = content.split("\n");
|
|
433
|
+
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.echomem\]\s*$/.test(line));
|
|
434
|
+
if (start < 0)
|
|
435
|
+
return false;
|
|
436
|
+
let end = start + 1;
|
|
437
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
438
|
+
end += 1;
|
|
439
|
+
const next = [...lines.slice(0, start), ...lines.slice(end)]
|
|
440
|
+
.join("\n")
|
|
441
|
+
.replace(/^\n+/, "")
|
|
442
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
443
|
+
atomicWriteTextFile(configPath, next);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
418
446
|
/**
|
|
419
447
|
* Write the EchoMem guidance block into the agent's GLOBAL memory file (~/.codex/AGENTS.md,
|
|
420
448
|
* ~/.claude/CLAUDE.md) so the agent treats EchoMem as its core memory tool without the tool
|
|
@@ -469,13 +497,35 @@ export function writeAgentsMemoryGuidance(filePath) {
|
|
|
469
497
|
const current = content.slice(start, end + AGENTS_MD_END.length);
|
|
470
498
|
if (current === block)
|
|
471
499
|
return "exists";
|
|
472
|
-
|
|
500
|
+
atomicWriteTextFile(filePath, content.slice(0, start) + block + content.slice(end + AGENTS_MD_END.length));
|
|
473
501
|
return "updated";
|
|
474
502
|
}
|
|
475
503
|
const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
476
|
-
|
|
504
|
+
atomicWriteTextFile(filePath, content + sep + block + "\n");
|
|
477
505
|
return "wrote";
|
|
478
506
|
}
|
|
507
|
+
/** Remove EchoMem's marker-owned guidance block without touching user-authored content. */
|
|
508
|
+
export function removeAgentsMemoryGuidance(filePath) {
|
|
509
|
+
let content;
|
|
510
|
+
try {
|
|
511
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (error.code === "ENOENT")
|
|
515
|
+
return false;
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
const start = content.indexOf(AGENTS_MD_BEGIN);
|
|
519
|
+
const end = content.indexOf(AGENTS_MD_END);
|
|
520
|
+
if (start < 0 || end <= start)
|
|
521
|
+
return false;
|
|
522
|
+
const next = (content.slice(0, start) + content.slice(end + AGENTS_MD_END.length))
|
|
523
|
+
.replace(/^\s*\n/, "")
|
|
524
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
525
|
+
.trimEnd();
|
|
526
|
+
atomicWriteTextFile(filePath, next ? `${next}\n` : "");
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
479
529
|
/**
|
|
480
530
|
* Refresh marker-owned guidance for users upgrading an existing standalone MCP install.
|
|
481
531
|
* This intentionally does not create global memory files: setup owns first installation,
|
|
@@ -507,32 +557,30 @@ export function refreshInstalledMemoryGuidance() {
|
|
|
507
557
|
}
|
|
508
558
|
/** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
|
|
509
559
|
export function writeJsonClientConfig(configPath, entry, options = {}) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
catch {
|
|
515
|
-
/* fresh config */
|
|
516
|
-
}
|
|
517
|
-
config.mcpServers = config.mcpServers || {};
|
|
518
|
-
if (!options.forceHeadless && validDesktopManagedEntry(config.mcpServers.echomem)) {
|
|
560
|
+
const config = readJsonObjectFile(configPath, "MCP client configuration");
|
|
561
|
+
const servers = objectRecord(config.mcpServers) ?? {};
|
|
562
|
+
config.mcpServers = servers;
|
|
563
|
+
if (!options.forceHeadless && validDesktopManagedEntry(servers.echomem)) {
|
|
519
564
|
return "desktop-managed";
|
|
520
565
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
566
|
+
servers.echomem = entry;
|
|
567
|
+
atomicWriteJsonObject(configPath, config);
|
|
524
568
|
return "wrote";
|
|
525
569
|
}
|
|
570
|
+
/** Remove only the EchoMem server entry from a JSON MCP client configuration. */
|
|
571
|
+
export function removeJsonClientConfig(configPath) {
|
|
572
|
+
if (!fs.existsSync(configPath))
|
|
573
|
+
return false;
|
|
574
|
+
const config = readJsonObjectFile(configPath, "MCP client configuration");
|
|
575
|
+
const servers = objectRecord(config.mcpServers);
|
|
576
|
+
if (!servers || !("echomem" in servers))
|
|
577
|
+
return false;
|
|
578
|
+
delete servers.echomem;
|
|
579
|
+
atomicWriteJsonObject(configPath, config);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
526
582
|
function readClaudeCodeConfigFile(configPath) {
|
|
527
|
-
|
|
528
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
529
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
530
|
-
? parsed
|
|
531
|
-
: {};
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
return {};
|
|
535
|
-
}
|
|
583
|
+
return readJsonObjectFile(configPath, "Claude Code user configuration");
|
|
536
584
|
}
|
|
537
585
|
function echoMemEntryFromServers(value) {
|
|
538
586
|
return objectRecord(objectRecord(value)?.echomem);
|
|
@@ -866,6 +914,36 @@ export function writeClaudeCodeConfig(entry, options = {}) {
|
|
|
866
914
|
usedDirectWrite,
|
|
867
915
|
};
|
|
868
916
|
}
|
|
917
|
+
/** Remove EchoMem at Claude Code user/project scope while preserving account and sibling config. */
|
|
918
|
+
export function removeClaudeCodeConfig(configPath = home(".claude.json")) {
|
|
919
|
+
if (!fs.existsSync(configPath))
|
|
920
|
+
return { removedUserEntry: false, removedProjectEntries: [] };
|
|
921
|
+
const config = readClaudeCodeConfigFile(configPath);
|
|
922
|
+
let changed = false;
|
|
923
|
+
let removedUserEntry = false;
|
|
924
|
+
const userServers = objectRecord(config.mcpServers);
|
|
925
|
+
if (userServers && "echomem" in userServers) {
|
|
926
|
+
delete userServers.echomem;
|
|
927
|
+
removedUserEntry = true;
|
|
928
|
+
changed = true;
|
|
929
|
+
}
|
|
930
|
+
const removedProjectEntries = [];
|
|
931
|
+
const projects = objectRecord(config.projects);
|
|
932
|
+
if (projects) {
|
|
933
|
+
for (const [projectPath, value] of Object.entries(projects)) {
|
|
934
|
+
const project = objectRecord(value);
|
|
935
|
+
const servers = objectRecord(project?.mcpServers);
|
|
936
|
+
if (!servers || !("echomem" in servers))
|
|
937
|
+
continue;
|
|
938
|
+
delete servers.echomem;
|
|
939
|
+
removedProjectEntries.push(projectPath);
|
|
940
|
+
changed = true;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (changed)
|
|
944
|
+
atomicWriteJsonObject(configPath, config);
|
|
945
|
+
return { removedUserEntry, removedProjectEntries };
|
|
946
|
+
}
|
|
869
947
|
function readJsonClientEntry(configPath) {
|
|
870
948
|
try {
|
|
871
949
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
@@ -3695,6 +3773,293 @@ function cmdLock() {
|
|
|
3695
3773
|
store.clearKey();
|
|
3696
3774
|
console.log("🔒 EchoMem vault locked on this device. Your login remains connected.");
|
|
3697
3775
|
}
|
|
3776
|
+
export async function collectMcpDoctorReport(options = {}) {
|
|
3777
|
+
const updateStatus = options.noNetwork
|
|
3778
|
+
? readCachedUpdateStatus()
|
|
3779
|
+
: await checkLatestUpdateStatus({ force: true });
|
|
3780
|
+
const desiredVersion = updateStatus?.latestVersion ?? MCP_PACKAGE_VERSION;
|
|
3781
|
+
const runtime = readHeadlessRuntimeInstallation();
|
|
3782
|
+
const store = new KeyStore();
|
|
3783
|
+
const token = store.getToken();
|
|
3784
|
+
const key = store.getKey();
|
|
3785
|
+
let account = token
|
|
3786
|
+
? { state: options.noNetwork ? "not_checked" : "unreachable", detail: options.noNetwork ? "Account validation skipped." : "Account validation did not finish." }
|
|
3787
|
+
: { state: "not_connected", detail: "Connect this Windows profile to an Echo account." };
|
|
3788
|
+
let vault = key
|
|
3789
|
+
? { state: options.noNetwork ? "not_checked" : "unknown", detail: options.noNetwork ? "Vault validation skipped." : "Checking the saved vault key." }
|
|
3790
|
+
: { state: token ? "unknown" : "locked", detail: token ? "Vault state has not been checked." : "Connect the account before checking the vault." };
|
|
3791
|
+
let lastSearch;
|
|
3792
|
+
if (token && !options.noNetwork) {
|
|
3793
|
+
const diagnostics = await loadHudDiagnostics();
|
|
3794
|
+
if (diagnostics.account.state === "connected") {
|
|
3795
|
+
account = {
|
|
3796
|
+
state: "connected",
|
|
3797
|
+
plan: diagnostics.account.plan,
|
|
3798
|
+
detail: diagnostics.account.plan
|
|
3799
|
+
? `${diagnostics.account.plan} account verified.`
|
|
3800
|
+
: "Echo account verified.",
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
else if (diagnostics.accountRequest.httpStatus === 401 || diagnostics.accountRequest.httpStatus === 403) {
|
|
3804
|
+
account = { state: "invalid", detail: "The saved Echo credential is no longer valid. Reconnect the account." };
|
|
3805
|
+
}
|
|
3806
|
+
else {
|
|
3807
|
+
account = { state: "unreachable", detail: diagnostics.account.error || "Echo account validation is temporarily unavailable." };
|
|
3808
|
+
}
|
|
3809
|
+
if (diagnostics.lastSearch) {
|
|
3810
|
+
lastSearch = {
|
|
3811
|
+
at: diagnostics.lastSearch.at,
|
|
3812
|
+
ok: diagnostics.lastSearch.ok,
|
|
3813
|
+
httpStatus: diagnostics.lastSearch.httpStatus,
|
|
3814
|
+
latencyMs: diagnostics.lastSearch.latencyMs,
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
if (account.state === "connected") {
|
|
3818
|
+
try {
|
|
3819
|
+
const encryption = await fetchEncryptionConfig(authedAxios(token));
|
|
3820
|
+
if (!encryption.enabled) {
|
|
3821
|
+
vault = { state: "unencrypted", detail: "This account does not require a local vault key." };
|
|
3822
|
+
}
|
|
3823
|
+
else if (!key) {
|
|
3824
|
+
vault = { state: "locked", detail: "The encrypted vault is locked on this Windows profile." };
|
|
3825
|
+
}
|
|
3826
|
+
else if (await verifyKeyB64(key, encryption)) {
|
|
3827
|
+
vault = { state: "unlocked", detail: "The local vault key is valid." };
|
|
3828
|
+
}
|
|
3829
|
+
else {
|
|
3830
|
+
vault = { state: "invalid", detail: "The saved vault key does not match this account. Reconnect and unlock again." };
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
catch (error) {
|
|
3834
|
+
vault = { state: "unknown", detail: `Vault validation unavailable: ${formatVerificationError(error)}` };
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
else if (!key) {
|
|
3838
|
+
vault = { state: "unknown", detail: "Vault state cannot be verified until the account reconnects." };
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
const wsl = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
|
|
3842
|
+
const clients = inspectClientConfigs(desiredVersion).map((client) => {
|
|
3843
|
+
const health = !client.configured
|
|
3844
|
+
? "not_configured"
|
|
3845
|
+
: client.state === "ok" || client.state === "newer"
|
|
3846
|
+
? "ready"
|
|
3847
|
+
: client.state === "stale" || client.state === "missing"
|
|
3848
|
+
? "repair"
|
|
3849
|
+
: "attention";
|
|
3850
|
+
return {
|
|
3851
|
+
...client,
|
|
3852
|
+
detected: true,
|
|
3853
|
+
health,
|
|
3854
|
+
recommendedAction: health === "not_configured" ? "connect" : health === "repair" || health === "attention" ? "repair" : "none",
|
|
3855
|
+
};
|
|
3856
|
+
});
|
|
3857
|
+
return {
|
|
3858
|
+
packageVersion: MCP_PACKAGE_VERSION,
|
|
3859
|
+
publishedLatest: updateStatus?.latestVersion,
|
|
3860
|
+
platform: process.platform,
|
|
3861
|
+
credentialsPresent: Boolean(token),
|
|
3862
|
+
vaultKeyPresent: Boolean(key),
|
|
3863
|
+
runtime: runtime ? { version: runtime.version, launcher: runtime.launcher } : undefined,
|
|
3864
|
+
account,
|
|
3865
|
+
vault,
|
|
3866
|
+
environment: {
|
|
3867
|
+
windowsNative: process.platform === "win32",
|
|
3868
|
+
wsl,
|
|
3869
|
+
detail: wsl
|
|
3870
|
+
? "This is WSL. Native Windows agents use a separate EchoMem installation and configuration."
|
|
3871
|
+
: process.platform === "win32"
|
|
3872
|
+
? "Native Windows profile. WSL installations are managed separately."
|
|
3873
|
+
: "This Connect Echo page is running outside native Windows.",
|
|
3874
|
+
},
|
|
3875
|
+
lastSearch,
|
|
3876
|
+
clients,
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* Remove standalone EchoMem MCP integration without deleting login/vault credentials or cloud data.
|
|
3881
|
+
* Every user-owned config is edited narrowly and atomically; malformed files fail closed.
|
|
3882
|
+
*/
|
|
3883
|
+
export function uninstallStandaloneMcp() {
|
|
3884
|
+
const store = new KeyStore();
|
|
3885
|
+
const report = {
|
|
3886
|
+
removedClients: [],
|
|
3887
|
+
removedGuidance: [],
|
|
3888
|
+
removedSkills: [],
|
|
3889
|
+
removedHookGroups: 0,
|
|
3890
|
+
removedRuntime: false,
|
|
3891
|
+
credentialsPreserved: Boolean(store.getToken() || store.getKey()),
|
|
3892
|
+
errors: [],
|
|
3893
|
+
};
|
|
3894
|
+
for (const client of knownClients()) {
|
|
3895
|
+
try {
|
|
3896
|
+
if (client.kind === "json") {
|
|
3897
|
+
if (removeJsonClientConfig(client.configPath))
|
|
3898
|
+
report.removedClients.push(client.label);
|
|
3899
|
+
}
|
|
3900
|
+
else if (client.kind === "command") {
|
|
3901
|
+
if (removeCodexConfig(client.configPath))
|
|
3902
|
+
report.removedClients.push(client.label);
|
|
3903
|
+
}
|
|
3904
|
+
else {
|
|
3905
|
+
const removed = removeClaudeCodeConfig();
|
|
3906
|
+
if (removed.removedUserEntry || removed.removedProjectEntries.length > 0)
|
|
3907
|
+
report.removedClients.push(client.label);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
catch (error) {
|
|
3911
|
+
report.errors.push(`${client.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
try {
|
|
3915
|
+
const hooks = removeLifecycleHooks("both");
|
|
3916
|
+
report.removedHookGroups = hooks.removedGroups;
|
|
3917
|
+
}
|
|
3918
|
+
catch (error) {
|
|
3919
|
+
report.errors.push(`Lifecycle hooks: ${error instanceof Error ? error.message : String(error)}`);
|
|
3920
|
+
}
|
|
3921
|
+
for (const file of [path.join(codexHome(), "AGENTS.md"), path.join(claudeConfigHome(), "CLAUDE.md")]) {
|
|
3922
|
+
try {
|
|
3923
|
+
if (removeAgentsMemoryGuidance(file))
|
|
3924
|
+
report.removedGuidance.push(file);
|
|
3925
|
+
}
|
|
3926
|
+
catch (error) {
|
|
3927
|
+
report.errors.push(`Guidance ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
for (const skillName of CODEX_SKILL_NAMES) {
|
|
3931
|
+
const directory = path.join(codexHome(), "skills", skillName);
|
|
3932
|
+
try {
|
|
3933
|
+
if (!fs.existsSync(directory))
|
|
3934
|
+
continue;
|
|
3935
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
3936
|
+
report.removedSkills.push(skillName);
|
|
3937
|
+
}
|
|
3938
|
+
catch (error) {
|
|
3939
|
+
report.errors.push(`Codex skill ${skillName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
try {
|
|
3943
|
+
report.removedRuntime = removeHeadlessRuntime().removed;
|
|
3944
|
+
}
|
|
3945
|
+
catch (error) {
|
|
3946
|
+
report.errors.push(`Managed runtime: ${error instanceof Error ? error.message : String(error)}`);
|
|
3947
|
+
}
|
|
3948
|
+
return report;
|
|
3949
|
+
}
|
|
3950
|
+
function requireClient(clientId) {
|
|
3951
|
+
const client = knownClients().find((candidate) => candidate.id === clientId);
|
|
3952
|
+
if (!client)
|
|
3953
|
+
throw new Error(`Unsupported MCP host: ${clientId}`);
|
|
3954
|
+
return client;
|
|
3955
|
+
}
|
|
3956
|
+
export async function connectStandaloneMcpHost(clientId, options = {}) {
|
|
3957
|
+
requireClient(clientId);
|
|
3958
|
+
const updateStatus = await checkLatestUpdateStatus({ force: true });
|
|
3959
|
+
const targetVersion = updateStatus?.latestVersion ?? MCP_PACKAGE_VERSION;
|
|
3960
|
+
installHeadlessRuntimeSync(targetVersion, {
|
|
3961
|
+
force: options.repair === true,
|
|
3962
|
+
prune: options.repair === true,
|
|
3963
|
+
});
|
|
3964
|
+
await cmdSetup({
|
|
3965
|
+
client: clientId,
|
|
3966
|
+
"skip-login": true,
|
|
3967
|
+
"skip-runtime-install": true,
|
|
3968
|
+
"continue-on-client-error": true,
|
|
3969
|
+
});
|
|
3970
|
+
return collectMcpDoctorReport({ noNetwork: true });
|
|
3971
|
+
}
|
|
3972
|
+
export async function disconnectStandaloneMcpHost(clientId) {
|
|
3973
|
+
const client = requireClient(clientId);
|
|
3974
|
+
if (client.kind === "json") {
|
|
3975
|
+
removeJsonClientConfig(client.configPath);
|
|
3976
|
+
}
|
|
3977
|
+
else if (client.kind === "command") {
|
|
3978
|
+
removeCodexConfig(client.configPath);
|
|
3979
|
+
}
|
|
3980
|
+
else {
|
|
3981
|
+
removeClaudeCodeConfig();
|
|
3982
|
+
}
|
|
3983
|
+
if (client.id === "codex") {
|
|
3984
|
+
removeLifecycleHooks("codex");
|
|
3985
|
+
removeAgentsMemoryGuidance(path.join(codexHome(), "AGENTS.md"));
|
|
3986
|
+
for (const skillName of CODEX_SKILL_NAMES) {
|
|
3987
|
+
fs.rmSync(path.join(codexHome(), "skills", skillName), { recursive: true, force: true });
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
if (client.id === "claude-code")
|
|
3991
|
+
removeLifecycleHooks("claude-code");
|
|
3992
|
+
if (client.id === "claude-code" || client.id === "claude-desktop") {
|
|
3993
|
+
const otherId = client.id === "claude-code" ? "claude-desktop" : "claude-code";
|
|
3994
|
+
const other = requireClient(otherId);
|
|
3995
|
+
const otherReport = inspectClientConfig(other, MCP_PACKAGE_VERSION);
|
|
3996
|
+
if (!otherReport?.configured)
|
|
3997
|
+
removeAgentsMemoryGuidance(path.join(claudeConfigHome(), "CLAUDE.md"));
|
|
3998
|
+
}
|
|
3999
|
+
return collectMcpDoctorReport({ noNetwork: true });
|
|
4000
|
+
}
|
|
4001
|
+
async function cleanReconnectStandaloneMcp() {
|
|
4002
|
+
const updateStatus = await checkLatestUpdateStatus({ force: true });
|
|
4003
|
+
const targetVersion = updateStatus?.latestVersion ?? MCP_PACKAGE_VERSION;
|
|
4004
|
+
console.log(`Installing a clean ${MCP_PACKAGE_NAME}@${targetVersion} runtime…`);
|
|
4005
|
+
installHeadlessRuntimeSync(targetVersion, { force: true, prune: true });
|
|
4006
|
+
await cmdSetup({
|
|
4007
|
+
all: true,
|
|
4008
|
+
"skip-login": true,
|
|
4009
|
+
"skip-runtime-install": true,
|
|
4010
|
+
"continue-on-client-error": true,
|
|
4011
|
+
});
|
|
4012
|
+
return collectMcpDoctorReport({ noNetwork: true });
|
|
4013
|
+
}
|
|
4014
|
+
async function cmdControl() {
|
|
4015
|
+
const control = await startMcpControlServer({
|
|
4016
|
+
doctor: () => collectMcpDoctorReport(),
|
|
4017
|
+
connectHost: (clientId, repair) => connectStandaloneMcpHost(clientId, { repair }),
|
|
4018
|
+
disconnectHost: (clientId) => disconnectStandaloneMcpHost(clientId),
|
|
4019
|
+
reconnect: () => cleanReconnectStandaloneMcp(),
|
|
4020
|
+
uninstall: () => uninstallStandaloneMcp(),
|
|
4021
|
+
});
|
|
4022
|
+
console.log("Connect Echo is running locally.");
|
|
4023
|
+
console.log(control.url);
|
|
4024
|
+
console.log("Close this terminal or press Ctrl+C when you are finished.");
|
|
4025
|
+
openBrowser(control.url);
|
|
4026
|
+
}
|
|
4027
|
+
async function cmdConnect(flags) {
|
|
4028
|
+
if (flags["skip-login"] !== true) {
|
|
4029
|
+
const current = await collectMcpDoctorReport();
|
|
4030
|
+
const accountNeedsLogin = current.account.state === "not_connected" || current.account.state === "invalid";
|
|
4031
|
+
const vaultNeedsUnlock = current.account.state === "connected"
|
|
4032
|
+
&& (current.vault.state === "locked" || current.vault.state === "invalid");
|
|
4033
|
+
if (accountNeedsLogin || vaultNeedsUnlock) {
|
|
4034
|
+
const connected = await cmdLogin({ ...flags, force: true });
|
|
4035
|
+
if (!connected)
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
await cmdControl();
|
|
4040
|
+
}
|
|
4041
|
+
async function cmdReconnect(flags) {
|
|
4042
|
+
const report = await cleanReconnectStandaloneMcp();
|
|
4043
|
+
const configured = report.clients.filter((client) => client.configured).length;
|
|
4044
|
+
console.log(`Clean reconnect complete: ${configured} MCP host${configured === 1 ? "" : "s"} configured.`);
|
|
4045
|
+
console.log("Start a new session in each MCP host to load the clean runtime.");
|
|
4046
|
+
}
|
|
4047
|
+
function cmdUninstall(flags) {
|
|
4048
|
+
if (flags.confirm !== true) {
|
|
4049
|
+
console.error("Uninstall requires explicit confirmation: echomem-mcp uninstall --confirm");
|
|
4050
|
+
console.error("This preserves EchoMem login, vault credentials, and cloud memories.");
|
|
4051
|
+
process.exitCode = 1;
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
const report = uninstallStandaloneMcp();
|
|
4055
|
+
console.log(`Removed EchoMem MCP from ${report.removedClients.length} host configuration${report.removedClients.length === 1 ? "" : "s"}.`);
|
|
4056
|
+
console.log(`Managed runtime: ${report.removedRuntime ? "removed" : "not installed"}.`);
|
|
4057
|
+
console.log("EchoMem login, vault credentials, and cloud memories were preserved.");
|
|
4058
|
+
if (report.errors.length > 0) {
|
|
4059
|
+
console.error(`Some components could not be removed:\n${report.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
4060
|
+
process.exitCode = 1;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
3698
4063
|
async function cmdStatus(flags = {}) {
|
|
3699
4064
|
const store = new KeyStore();
|
|
3700
4065
|
const token = store.getToken();
|
|
@@ -3815,6 +4180,7 @@ function cmdLogout() {
|
|
|
3815
4180
|
const HELP = `EchoMem MCP — local memory bridge
|
|
3816
4181
|
|
|
3817
4182
|
Usage:
|
|
4183
|
+
echomem-mcp connect Open Connect Echo; authenticate if needed, then manage MCP hosts
|
|
3818
4184
|
echomem-mcp init Legacy/headless setup: configure agents + login + local-history onboarding
|
|
3819
4185
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
3820
4186
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
@@ -3829,6 +4195,9 @@ Usage:
|
|
|
3829
4195
|
echomem-mcp lock Remove the local vault key while keeping the device login
|
|
3830
4196
|
echomem-mcp status Show token/key/clients
|
|
3831
4197
|
echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
|
|
4198
|
+
echomem-mcp control Alias for the returning-user Connect Echo control page
|
|
4199
|
+
echomem-mcp reconnect Clean-reinstall the managed runtime and repair detected hosts
|
|
4200
|
+
echomem-mcp uninstall --confirm Remove MCP integration but preserve credentials and cloud memories
|
|
3832
4201
|
echomem-mcp logout Remove stored credentials
|
|
3833
4202
|
echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
|
|
3834
4203
|
echomem-mcp migrate --estimate Estimate migration size/time metadata without uploading transcripts
|
|
@@ -3863,6 +4232,9 @@ export async function runCli(argv) {
|
|
|
3863
4232
|
}
|
|
3864
4233
|
}
|
|
3865
4234
|
switch (cmd) {
|
|
4235
|
+
case "connect":
|
|
4236
|
+
await cmdConnect(flags);
|
|
4237
|
+
return true;
|
|
3866
4238
|
case "init":
|
|
3867
4239
|
await cmdInit(flags);
|
|
3868
4240
|
return true;
|
|
@@ -3887,6 +4259,17 @@ export async function runCli(argv) {
|
|
|
3887
4259
|
case "doctor":
|
|
3888
4260
|
cmdDoctor();
|
|
3889
4261
|
return true;
|
|
4262
|
+
case "control":
|
|
4263
|
+
case "manage":
|
|
4264
|
+
await cmdControl();
|
|
4265
|
+
return true;
|
|
4266
|
+
case "reconnect":
|
|
4267
|
+
case "repair":
|
|
4268
|
+
await cmdReconnect(flags);
|
|
4269
|
+
return true;
|
|
4270
|
+
case "uninstall":
|
|
4271
|
+
cmdUninstall(flags);
|
|
4272
|
+
return true;
|
|
3890
4273
|
case "logout":
|
|
3891
4274
|
cmdLogout();
|
|
3892
4275
|
return true;
|