@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/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 { installHeadlessRuntimeSync, readHeadlessRuntimeInstallation, } from "./headless-runtime.js";
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
- fs.writeFileSync(configPath, next.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, ""));
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
- fs.appendFileSync(configPath, sep + block + "\n");
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
- fs.writeFileSync(filePath, content.slice(0, start) + block + content.slice(end + AGENTS_MD_END.length));
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
- fs.appendFileSync(filePath, sep + block + "\n");
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
- let config = {};
511
- try {
512
- config = JSON.parse(fs.readFileSync(configPath, "utf8"));
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
- config.mcpServers.echomem = entry;
522
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
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
- try {
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;