@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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
function connectEchoPage(nonce) {
|
|
4
|
+
const safeNonce = JSON.stringify(nonce);
|
|
5
|
+
return `<!doctype html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
|
+
<title>Connect Echo</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root { color-scheme:light; --ink:#172019; --muted:#667168; --line:#dce4da; --leaf:#315d38; --leaf-soft:#edf5eb; --paper:#f6f7f2; --warn:#9b392b; --warn-soft:#faebe7; --amber:#8a611b; --amber-soft:#fff4d8; }
|
|
13
|
+
* { box-sizing:border-box; }
|
|
14
|
+
body { margin:0; background:radial-gradient(circle at 14% 0%,#eef4e8 0,transparent 33%),var(--paper); color:var(--ink); font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; }
|
|
15
|
+
main { width:min(980px,calc(100% - 32px)); margin:38px auto 80px; }
|
|
16
|
+
h1,h2,h3,p { margin:0; } h1 { font-size:clamp(34px,5vw,58px); letter-spacing:-.05em; line-height:.98; }
|
|
17
|
+
button { border:0; border-radius:11px; padding:10px 14px; font:inherit; font-weight:800; cursor:pointer; }
|
|
18
|
+
button:disabled { cursor:wait; opacity:.52; }
|
|
19
|
+
.primary { color:white; background:var(--leaf); } .secondary { color:var(--ink); background:#eef1eb; } .danger { color:white; background:var(--warn); }
|
|
20
|
+
.top { display:flex; justify-content:space-between; align-items:end; gap:22px; margin-bottom:20px; }
|
|
21
|
+
.eyebrow { color:var(--leaf); font-size:12px; font-weight:850; letter-spacing:.14em; text-transform:uppercase; margin-bottom:8px; }
|
|
22
|
+
.local { border:1px solid var(--line); border-radius:999px; padding:7px 11px; color:var(--muted); background:rgba(255,255,255,.85); white-space:nowrap; }
|
|
23
|
+
.panel { background:rgba(255,255,255,.94); border:1px solid var(--line); border-radius:20px; padding:20px; box-shadow:0 16px 50px rgba(34,54,35,.065); margin-top:14px; }
|
|
24
|
+
.account { display:grid; grid-template-columns:1fr auto; align-items:center; gap:18px; }
|
|
25
|
+
.accountTitle { display:flex; align-items:center; gap:9px; margin-bottom:5px; }
|
|
26
|
+
.accountTitle strong { font-size:20px; } .sub,.note,.host p { color:var(--muted); }
|
|
27
|
+
.facts { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px; }
|
|
28
|
+
.pill,.badge { border-radius:999px; padding:5px 9px; background:var(--leaf-soft); color:#2d6337; font-size:12px; font-weight:850; }
|
|
29
|
+
.pill.bad,.badge.bad { color:var(--warn); background:var(--warn-soft); } .pill.warn,.badge.warn { color:var(--amber); background:var(--amber-soft); }
|
|
30
|
+
.environment { display:none; margin-top:12px; border-radius:12px; background:var(--amber-soft); color:#6d4c15; padding:10px 12px; }
|
|
31
|
+
.sectionHead { display:flex; justify-content:space-between; align-items:center; gap:16px; margin-bottom:14px; }
|
|
32
|
+
.sectionHead h2 { font-size:22px; letter-spacing:-.02em; }
|
|
33
|
+
.hosts { display:grid; grid-template-columns:repeat(auto-fit,minmax(255px,1fr)); gap:12px; }
|
|
34
|
+
.host { border:1px solid var(--line); border-radius:16px; padding:16px; min-height:190px; display:flex; flex-direction:column; }
|
|
35
|
+
.hostHead { display:flex; justify-content:space-between; align-items:center; gap:10px; }
|
|
36
|
+
.host h3 { font-size:17px; } .host p { margin-top:8px; font-size:13px; overflow-wrap:anywhere; }
|
|
37
|
+
.host code { display:block; margin-top:7px; color:#788179; font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace; overflow-wrap:anywhere; }
|
|
38
|
+
.hostActions { display:flex; flex-wrap:wrap; gap:8px; margin-top:auto; padding-top:15px; }
|
|
39
|
+
.empty { padding:24px; border:1px dashed #cbd5c9; border-radius:14px; color:var(--muted); }
|
|
40
|
+
.recovery { display:grid; grid-template-columns:1fr auto; align-items:center; gap:18px; }
|
|
41
|
+
.recovery h2 { font-size:20px; } .recovery p { color:var(--muted); margin-top:5px; max-width:650px; }
|
|
42
|
+
.advanced { margin-top:14px; border-top:1px solid var(--line); padding-top:13px; }
|
|
43
|
+
.advanced summary { cursor:pointer; color:var(--muted); font-weight:750; }
|
|
44
|
+
.advancedBody { display:flex; justify-content:space-between; align-items:center; gap:18px; margin-top:12px; }
|
|
45
|
+
.advancedBody p { color:var(--muted); max-width:680px; }
|
|
46
|
+
#message { min-height:25px; margin:14px 2px 0; color:var(--muted); }
|
|
47
|
+
@media(max-width:700px) { .top { align-items:start; flex-direction:column; } .account,.recovery { grid-template-columns:1fr; } .facts { justify-content:flex-start; } .advancedBody { align-items:start; flex-direction:column; } }
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<main>
|
|
52
|
+
<header class="top"><div><p class="eyebrow">Windows MCP control center</p><h1>Connect Echo</h1></div><span class="local">Localhost only</span></header>
|
|
53
|
+
<section class="panel account" aria-live="polite">
|
|
54
|
+
<div><div class="accountTitle"><strong id="accountHeadline">Checking this profile…</strong><span class="pill warn" id="accountBadge">Checking</span></div><p class="sub" id="accountDetail">Validating the account, vault, and managed runtime.</p><p class="environment" id="environment"></p></div>
|
|
55
|
+
<div class="facts"><span class="pill" id="runtimeFact">Runtime —</span><span class="pill" id="vaultFact">Vault —</span><span class="pill" id="activityFact">No tool activity yet</span></div>
|
|
56
|
+
</section>
|
|
57
|
+
<section class="panel">
|
|
58
|
+
<div class="sectionHead"><div><h2>Your AI tools</h2><p class="note">Connect, inspect, or repair each detected Windows host independently.</p></div><button class="secondary" id="refresh">Run Doctor</button></div>
|
|
59
|
+
<div class="hosts" id="hosts"><p class="empty">Detecting supported MCP hosts…</p></div>
|
|
60
|
+
</section>
|
|
61
|
+
<section class="panel recovery">
|
|
62
|
+
<div><h2>Clean reconnect</h2><p>Install a validated latest runtime, clear inactive EchoMem-managed versions, and repair every detected host. Credentials and cloud memories stay intact.</p></div>
|
|
63
|
+
<button class="primary" id="reconnect">Reconnect all</button>
|
|
64
|
+
</section>
|
|
65
|
+
<details class="panel advanced">
|
|
66
|
+
<summary>Advanced controls</summary>
|
|
67
|
+
<div class="advancedBody"><p>Uninstall removes EchoMem host registrations, lifecycle hooks, guidance, Codex skills, and the managed runtime. It preserves the account credential, vault key, and cloud memories.</p><button class="danger" id="uninstall">Uninstall MCP…</button></div>
|
|
68
|
+
</details>
|
|
69
|
+
<p id="message" role="status"></p>
|
|
70
|
+
</main>
|
|
71
|
+
<script>
|
|
72
|
+
(() => {
|
|
73
|
+
const nonce = ${safeNonce};
|
|
74
|
+
const message = document.getElementById("message");
|
|
75
|
+
const buttons = () => Array.from(document.querySelectorAll("button"));
|
|
76
|
+
const esc = (value) => String(value == null ? "" : value).replace(/[&<>"']/g, (ch) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[ch]));
|
|
77
|
+
const busy = (state) => buttons().forEach((button) => { button.disabled = state; });
|
|
78
|
+
async function request(path, method, payload) {
|
|
79
|
+
const response = await fetch(path + "?nonce=" + encodeURIComponent(nonce), {
|
|
80
|
+
method: method || "GET", credentials:"omit", cache:"no-store",
|
|
81
|
+
headers:{"Content-Type":"application/json"}, body:method === "POST" ? JSON.stringify(Object.assign({nonce},payload || {})) : undefined
|
|
82
|
+
});
|
|
83
|
+
const raw = await response.text(); let data = {};
|
|
84
|
+
try { data = raw ? JSON.parse(raw) : {}; } catch (_) {}
|
|
85
|
+
if (!response.ok) throw new Error(data.message || raw || ("HTTP " + response.status));
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
function tone(state) {
|
|
89
|
+
return state === "connected" || state === "unlocked" || state === "unencrypted" || state === "ready" ? "" : state === "unreachable" || state === "unknown" || state === "attention" || state === "not_checked" ? "warn" : "bad";
|
|
90
|
+
}
|
|
91
|
+
function label(value) { return String(value || "unknown").replace(/_/g," "); }
|
|
92
|
+
function render(report) {
|
|
93
|
+
const account = report.account || {state:report.credentialsPresent ? "not_checked" : "not_connected",detail:""};
|
|
94
|
+
const vault = report.vault || {state:report.vaultKeyPresent ? "not_checked" : "unknown",detail:""};
|
|
95
|
+
const accountBadge = document.getElementById("accountBadge");
|
|
96
|
+
document.getElementById("accountHeadline").textContent = account.state === "connected" ? "Echo account connected" : account.state === "not_connected" ? "Echo account not connected" : account.state === "invalid" ? "Echo account needs reconnecting" : "Echo account check needs attention";
|
|
97
|
+
accountBadge.textContent = label(account.state);
|
|
98
|
+
accountBadge.className = "pill " + tone(account.state);
|
|
99
|
+
document.getElementById("accountDetail").textContent = account.detail || "";
|
|
100
|
+
const runtime = report.runtime && report.runtime.version ? report.runtime.version : "not installed";
|
|
101
|
+
const runtimeFact = document.getElementById("runtimeFact"); runtimeFact.textContent = "Runtime " + runtime; runtimeFact.className = "pill " + (report.runtime ? "" : "bad");
|
|
102
|
+
const vaultFact = document.getElementById("vaultFact"); vaultFact.textContent = "Vault " + label(vault.state); vaultFact.className = "pill " + tone(vault.state);
|
|
103
|
+
const activity = document.getElementById("activityFact");
|
|
104
|
+
activity.textContent = report.lastSearch ? (report.lastSearch.ok ? "Last search succeeded" : "Last search failed") : "No recent search";
|
|
105
|
+
activity.className = "pill " + (report.lastSearch && report.lastSearch.ok === false ? "bad" : report.lastSearch ? "" : "warn");
|
|
106
|
+
const environment = document.getElementById("environment");
|
|
107
|
+
environment.textContent = report.environment && report.environment.detail || "";
|
|
108
|
+
environment.style.display = report.environment && (report.environment.wsl || !report.environment.windowsNative) ? "block" : "none";
|
|
109
|
+
|
|
110
|
+
const clients = Array.isArray(report.clients) ? report.clients : [];
|
|
111
|
+
document.getElementById("hosts").innerHTML = clients.length ? clients.map((client) => {
|
|
112
|
+
const action = client.recommendedAction || (!client.configured ? "connect" : client.state === "ok" ? "none" : "repair");
|
|
113
|
+
const primary = action === "connect" ? '<button class="primary" data-host-action="connect" data-client-id="' + esc(client.id) + '">Connect</button>' : action === "repair" ? '<button class="primary" data-host-action="repair" data-client-id="' + esc(client.id) + '">Repair</button>' : '';
|
|
114
|
+
const disconnect = client.configured ? '<button class="secondary" data-host-action="disconnect" data-client-id="' + esc(client.id) + '">Disconnect</button>' : '';
|
|
115
|
+
const version = client.version || (client.runtime === "latest" ? "latest at launch" : "version unknown");
|
|
116
|
+
return '<article class="host"><div class="hostHead"><h3>' + esc(client.label) + '</h3><span class="badge ' + tone(client.health) + '">' + esc(label(client.health)) + '</span></div><p>' + esc(client.configured ? "EchoMem MCP is configured for this host." : "Detected on this profile, but EchoMem is not configured.") + '</p><p>' + esc(version) + '</p><code>' + esc(client.detail || "") + '</code><div class="hostActions">' + primary + disconnect + '</div></article>';
|
|
117
|
+
}).join("") : '<p class="empty">No supported MCP host was detected in this profile. Native Windows and WSL have separate installations.</p>';
|
|
118
|
+
}
|
|
119
|
+
async function doctor(statusText) { busy(true); message.textContent = statusText || "Running MCP Doctor…"; try { const data = await request("/doctor"); render(data); message.textContent = "Doctor finished. No configuration was changed."; return data; } catch (error) { message.textContent = error.message; } finally { busy(false); } }
|
|
120
|
+
document.getElementById("refresh").onclick = () => void doctor();
|
|
121
|
+
document.getElementById("hosts").onclick = async (event) => {
|
|
122
|
+
const button = event.target && event.target.closest ? event.target.closest("[data-host-action]") : null;
|
|
123
|
+
if (!button) return;
|
|
124
|
+
const clientId = button.getAttribute("data-client-id"); const action = button.getAttribute("data-host-action");
|
|
125
|
+
if (action === "disconnect" && !confirm("Disconnect EchoMem from this host? Your account and memories will be preserved.")) return;
|
|
126
|
+
busy(true); message.textContent = action === "disconnect" ? "Disconnecting this host…" : action === "repair" ? "Repairing this host…" : "Connecting this host…";
|
|
127
|
+
try { await request(action === "disconnect" ? "/disconnect-host" : "/connect-host","POST",{clientId,repair:action === "repair"}); await doctor(); message.textContent = action === "disconnect" ? "Host disconnected. Restart it to unload EchoMem." : "Host configured. Start a new session in it to load EchoMem."; } catch (error) { message.textContent = error.message; } finally { busy(false); }
|
|
128
|
+
};
|
|
129
|
+
document.getElementById("reconnect").onclick = async () => { if (!confirm("Reconnect every detected host with a clean managed runtime? Active sessions keep their current runtime until restarted.")) return; busy(true); message.textContent = "Installing a clean runtime and repairing detected hosts…"; try { await request("/reconnect","POST"); await doctor(); message.textContent = "Clean reconnect complete. Restart each host to load the new runtime."; } catch (error) { message.textContent = error.message; } finally { busy(false); } };
|
|
130
|
+
document.getElementById("uninstall").onclick = async () => { if (!confirm("Uninstall EchoMem MCP from this profile? Login, vault credentials, and cloud memories will be preserved.")) return; busy(true); message.textContent = "Removing EchoMem MCP components…"; try { await request("/uninstall","POST"); await doctor(); message.textContent = "EchoMem MCP removed. Credentials and cloud memories were preserved."; } catch (error) { message.textContent = error.message; } finally { busy(false); } };
|
|
131
|
+
void doctor("Checking this Windows profile…");
|
|
132
|
+
})();
|
|
133
|
+
</script>
|
|
134
|
+
</body></html>`;
|
|
135
|
+
}
|
|
136
|
+
function readJsonBody(req) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const chunks = [];
|
|
139
|
+
let size = 0;
|
|
140
|
+
req.on("data", (chunk) => {
|
|
141
|
+
size += chunk.length;
|
|
142
|
+
if (size > 4096) {
|
|
143
|
+
reject(new Error("Request too large"));
|
|
144
|
+
req.destroy();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
chunks.push(chunk);
|
|
148
|
+
});
|
|
149
|
+
req.on("end", () => {
|
|
150
|
+
try {
|
|
151
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
152
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
153
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
154
|
+
throw new Error("Invalid JSON body");
|
|
155
|
+
resolve(parsed);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
reject(error);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
req.on("error", reject);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export async function startMcpControlServer(actions) {
|
|
165
|
+
const nonce = randomUUID();
|
|
166
|
+
let operation = null;
|
|
167
|
+
const server = http.createServer((req, res) => {
|
|
168
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
169
|
+
const json = (status, body) => res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" }).end(JSON.stringify(body));
|
|
170
|
+
const run = async () => {
|
|
171
|
+
if (url.searchParams.get("nonce") !== nonce)
|
|
172
|
+
return void json(403, { message: "Invalid local control nonce." });
|
|
173
|
+
if (url.pathname === "/" && req.method === "GET")
|
|
174
|
+
return void res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }).end(connectEchoPage(nonce));
|
|
175
|
+
if (url.pathname === "/doctor" && req.method === "GET")
|
|
176
|
+
return void json(200, await actions.doctor());
|
|
177
|
+
if (req.method !== "POST")
|
|
178
|
+
return void json(404, { message: "Not found." });
|
|
179
|
+
const body = await readJsonBody(req);
|
|
180
|
+
if (operation)
|
|
181
|
+
return void json(409, { message: "Another Connect Echo operation is still running." });
|
|
182
|
+
const clientId = typeof body.clientId === "string" ? body.clientId : "";
|
|
183
|
+
const action = url.pathname === "/connect-host"
|
|
184
|
+
? () => actions.connectHost(clientId, body.repair === true)
|
|
185
|
+
: url.pathname === "/disconnect-host"
|
|
186
|
+
? () => actions.disconnectHost(clientId)
|
|
187
|
+
: url.pathname === "/reconnect"
|
|
188
|
+
? actions.reconnect
|
|
189
|
+
: url.pathname === "/uninstall"
|
|
190
|
+
? actions.uninstall
|
|
191
|
+
: null;
|
|
192
|
+
if (!action)
|
|
193
|
+
return void json(404, { message: "Not found." });
|
|
194
|
+
operation = Promise.resolve().then(action);
|
|
195
|
+
try {
|
|
196
|
+
return void json(200, await operation);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
operation = null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
run().catch((error) => {
|
|
203
|
+
if (!res.writableEnded)
|
|
204
|
+
json(500, { message: error instanceof Error ? error.message : String(error) });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
await new Promise((resolve, reject) => {
|
|
208
|
+
server.once("error", reject);
|
|
209
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
210
|
+
});
|
|
211
|
+
const address = server.address();
|
|
212
|
+
if (!address || typeof address === "string")
|
|
213
|
+
throw new Error("Could not bind the local Connect Echo page.");
|
|
214
|
+
return { url: `http://127.0.0.1:${address.port}/?nonce=${encodeURIComponent(nonce)}`, close: () => server.close() };
|
|
215
|
+
}
|