@echomem/mcp 1.4.49 → 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/durable-entry.js +122 -0
- package/dist/headless-runtime.js +59 -8
- package/dist/hud/hooks.js +102 -37
- package/dist/index.js +129 -12
- package/dist/local-data-paths.js +1 -1
- package/dist/mcp-control.js +215 -0
- package/dist/migrate.js +36 -3
- package/dist/setup.js +670 -116
- package/dist/source-session.js +253 -75
- package/dist/v1-contract.js +34 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
6
|
import { ZodError } from "zod";
|
|
7
|
-
import { bindSourceSessionSchema, canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, requestGroupSessionSharingSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
7
|
+
import { bindSourceSessionSchema, canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, linkWorkspaceTicketSessionSchema, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, requestGroupSessionSharingSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
8
8
|
import { KeyStore } from "./keystore.js";
|
|
9
9
|
import { EventLogger, hashText } from "./events.js";
|
|
10
10
|
import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
|
|
@@ -16,7 +16,7 @@ import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTI
|
|
|
16
16
|
import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
|
|
17
17
|
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
18
18
|
import { autoUpdateHeadlessRuntime } from "./headless-runtime.js";
|
|
19
|
-
import {
|
|
19
|
+
import { resolveSourceSessionRequestContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
|
|
20
20
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
21
21
|
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
|
|
22
22
|
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
@@ -582,6 +582,14 @@ function inputAnalyticsForTool(canonicalName, args) {
|
|
|
582
582
|
receipt_id_hash: receiptId ? hashText(receiptId) : undefined,
|
|
583
583
|
};
|
|
584
584
|
}
|
|
585
|
+
case canonicalToolNames.linkWorkspaceTicketSession: {
|
|
586
|
+
const ticketId = readString(a, "ticketId");
|
|
587
|
+
const workspaceId = readString(a, "workspaceId");
|
|
588
|
+
return {
|
|
589
|
+
ticket_id_hash: ticketId ? hashText(ticketId) : undefined,
|
|
590
|
+
workspace_id_hash: workspaceId ? hashText(workspaceId) : undefined,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
585
593
|
case canonicalToolNames.flagPublicationAttention: {
|
|
586
594
|
const memoryIds = Array.isArray(a.memoryIds) ? a.memoryIds.filter((id) => typeof id === "string") : [];
|
|
587
595
|
return {
|
|
@@ -609,13 +617,22 @@ function inputAnalyticsForTool(canonicalName, args) {
|
|
|
609
617
|
function toolEventName(canonicalName, status) {
|
|
610
618
|
return `[MCP] ${canonicalName} ${status}`;
|
|
611
619
|
}
|
|
620
|
+
const SUBAGENT_DURABLE_WRITE_MESSAGE = [
|
|
621
|
+
"Codex subagents cannot save EchoMem memories or manage conversation sharing.",
|
|
622
|
+
"Return durable findings to the root agent; the root agent must consolidate them and call save_conversation once.",
|
|
623
|
+
].join(" ");
|
|
624
|
+
const SUBAGENT_ROOT_ONLY_TOOLS = new Set([
|
|
625
|
+
canonicalToolNames.save,
|
|
626
|
+
canonicalToolNames.requestGroupSessionSharing,
|
|
627
|
+
canonicalToolNames.setGroupSessionSharing,
|
|
628
|
+
]);
|
|
612
629
|
class EchoMemApiClient {
|
|
613
630
|
store;
|
|
614
631
|
axios;
|
|
615
632
|
activeToken;
|
|
616
633
|
accountGeneration = 0;
|
|
617
634
|
boundSourceSession = null;
|
|
618
|
-
|
|
635
|
+
requestContext = new AsyncLocalStorage();
|
|
619
636
|
sourceSessionsByCanonicalKey = new Map();
|
|
620
637
|
whoamiCache = null;
|
|
621
638
|
/** One id per bridge process — groups all saves from this coding session under a single EchoMem context. */
|
|
@@ -677,10 +694,20 @@ class EchoMemApiClient {
|
|
|
677
694
|
}
|
|
678
695
|
getBoundSourceSession() {
|
|
679
696
|
this.synchronizeAccountContext();
|
|
680
|
-
|
|
697
|
+
const requestContext = this.requestContext.getStore();
|
|
698
|
+
return requestContext === undefined ? this.boundSourceSession : requestContext.sourceSession;
|
|
699
|
+
}
|
|
700
|
+
getRequestVerifiedSourceSession() {
|
|
701
|
+
const requestContext = this.requestContext.getStore();
|
|
702
|
+
if (!requestContext || requestContext.lineageStatus === "unbound")
|
|
703
|
+
return null;
|
|
704
|
+
return requestContext.sourceSession;
|
|
681
705
|
}
|
|
682
|
-
|
|
683
|
-
return
|
|
706
|
+
isSubagentRequest() {
|
|
707
|
+
return this.requestContext.getStore()?.isSubagent === true;
|
|
708
|
+
}
|
|
709
|
+
async withRequestContext(requestContext, operation) {
|
|
710
|
+
return this.requestContext.run(requestContext, operation);
|
|
684
711
|
}
|
|
685
712
|
async bindSourceSession(verified, persistForBridge = true) {
|
|
686
713
|
this.synchronizeAccountContext();
|
|
@@ -714,6 +741,21 @@ class EchoMemApiClient {
|
|
|
714
741
|
created: response.data?.created === true,
|
|
715
742
|
};
|
|
716
743
|
}
|
|
744
|
+
async linkWorkspaceTicketSession(args) {
|
|
745
|
+
const parsed = linkWorkspaceTicketSessionSchema.parse(args ?? {});
|
|
746
|
+
// A bridge-level compatibility binding can outlive a conversation in
|
|
747
|
+
// long-running hosts. Ticket links are therefore allowed to take the fast
|
|
748
|
+
// path only from identity verified for this exact request; otherwise the
|
|
749
|
+
// Desktop transcript watcher completes the link from local evidence.
|
|
750
|
+
const sourceSession = this.getRequestVerifiedSourceSession();
|
|
751
|
+
if (!sourceSession)
|
|
752
|
+
return null;
|
|
753
|
+
const response = await this.axios.post(`/api/extension/workspace-tickets/${encodeURIComponent(parsed.ticketId)}/sessions`, {
|
|
754
|
+
contextId: sourceSession.contextId,
|
|
755
|
+
workspaceId: parsed.workspaceId,
|
|
756
|
+
});
|
|
757
|
+
return response.data;
|
|
758
|
+
}
|
|
717
759
|
async trackMcpAnalyticsEvent(eventType, eventProperties, insertId) {
|
|
718
760
|
if (!this.hasToken())
|
|
719
761
|
return;
|
|
@@ -966,6 +1008,9 @@ class EchoMemApiClient {
|
|
|
966
1008
|
return data;
|
|
967
1009
|
}
|
|
968
1010
|
async saveConversation(args) {
|
|
1011
|
+
if (this.isSubagentRequest()) {
|
|
1012
|
+
throw new Error(SUBAGENT_DURABLE_WRITE_MESSAGE);
|
|
1013
|
+
}
|
|
969
1014
|
const parsed = saveConversationSchema.parse(args ?? {});
|
|
970
1015
|
const groupSharingScopeId = parsed.groupSharingScopeId ?? randomUUID();
|
|
971
1016
|
let rawData = parsed.conversation?.trim() || "";
|
|
@@ -984,6 +1029,7 @@ class EchoMemApiClient {
|
|
|
984
1029
|
const config = {
|
|
985
1030
|
headers: {
|
|
986
1031
|
"X-EchoMem-Request-Id": randomUUID(),
|
|
1032
|
+
"X-EchoMem-Origin-Channel": "local_mcp",
|
|
987
1033
|
...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
|
|
988
1034
|
},
|
|
989
1035
|
};
|
|
@@ -1429,21 +1475,32 @@ class EchoMemMCPServer {
|
|
|
1429
1475
|
const clientVersion = this.server.getClientVersion();
|
|
1430
1476
|
this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
|
|
1431
1477
|
this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
|
|
1432
|
-
let
|
|
1433
|
-
|
|
1478
|
+
let sourceResolution = {
|
|
1479
|
+
sourceSession: null,
|
|
1480
|
+
isSubagent: false,
|
|
1481
|
+
lineageStatus: "unbound",
|
|
1482
|
+
};
|
|
1434
1483
|
try {
|
|
1435
|
-
|
|
1484
|
+
sourceResolution = resolveSourceSessionRequestContext(extra._meta, { hostPlatform: this.getMcpClientAnalytics().host_platform });
|
|
1436
1485
|
}
|
|
1437
1486
|
catch {
|
|
1438
1487
|
// Malformed host metadata is not model input. Ignore it and retain the explicit fallback.
|
|
1439
1488
|
}
|
|
1489
|
+
// A child request starts fail-closed. Resolved children bind to the originating root; unresolved
|
|
1490
|
+
// children carry an explicit null so AsyncLocalStorage cannot fall through to a global binding.
|
|
1491
|
+
let requestSourceSession = sourceResolution.isSubagent
|
|
1492
|
+
? null
|
|
1493
|
+
: this.client.getBoundSourceSession();
|
|
1440
1494
|
// Verified request metadata always wins over a bridge-level compatibility fallback. The
|
|
1441
1495
|
// startup hook performs the eager write; this path attaches the exact context to the current
|
|
1442
1496
|
// request and retries the backend write if startup raced login.
|
|
1443
|
-
if (
|
|
1444
|
-
requestSourceSession = await this.client.bindSourceSession(
|
|
1497
|
+
if (sourceResolution.sourceSession && this.client.hasToken()) {
|
|
1498
|
+
requestSourceSession = await this.client.bindSourceSession(sourceResolution.sourceSession, false);
|
|
1445
1499
|
}
|
|
1446
|
-
return this.client.
|
|
1500
|
+
return this.client.withRequestContext({
|
|
1501
|
+
...sourceResolution,
|
|
1502
|
+
sourceSession: requestSourceSession,
|
|
1503
|
+
}, async () => {
|
|
1447
1504
|
const t0 = Date.now();
|
|
1448
1505
|
const analyticsBase = {
|
|
1449
1506
|
surface: "mcp",
|
|
@@ -1454,6 +1511,18 @@ class EchoMemMCPServer {
|
|
|
1454
1511
|
codex_session_id: this.client.getBoundSourceSession()?.canonicalKey ?? this.client.getSessionId(),
|
|
1455
1512
|
conversation_id: this.client.getBoundSourceSession()?.canonicalKey ?? this.client.getSessionId(),
|
|
1456
1513
|
context_id: this.client.getBoundSourceSession()?.contextId,
|
|
1514
|
+
agent_is_subagent: sourceResolution.isSubagent,
|
|
1515
|
+
agent_lineage_status: sourceResolution.lineageStatus,
|
|
1516
|
+
agent_depth: sourceResolution.agentDepth,
|
|
1517
|
+
agent_thread_id_hash: sourceResolution.agentThreadId
|
|
1518
|
+
? hashText(sourceResolution.agentThreadId)
|
|
1519
|
+
: undefined,
|
|
1520
|
+
agent_parent_thread_id_hash: sourceResolution.parentThreadId
|
|
1521
|
+
? hashText(sourceResolution.parentThreadId)
|
|
1522
|
+
: undefined,
|
|
1523
|
+
agent_root_thread_id_hash: sourceResolution.rootThreadId
|
|
1524
|
+
? hashText(sourceResolution.rootThreadId)
|
|
1525
|
+
: undefined,
|
|
1457
1526
|
tool_name: request.params.name,
|
|
1458
1527
|
canonical_tool_name: canonicalName,
|
|
1459
1528
|
...triggerAnalyticsForTool(canonicalName, toolArgs),
|
|
@@ -1471,6 +1540,13 @@ class EchoMemMCPServer {
|
|
|
1471
1540
|
if (recallRoute.error) {
|
|
1472
1541
|
throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
|
|
1473
1542
|
}
|
|
1543
|
+
if (sourceResolution.isSubagent && SUBAGENT_ROOT_ONLY_TOOLS.has(canonicalName)) {
|
|
1544
|
+
rec.error_kind = "invalid_args";
|
|
1545
|
+
return {
|
|
1546
|
+
content: [{ type: "text", text: SUBAGENT_DURABLE_WRITE_MESSAGE }],
|
|
1547
|
+
isError: true,
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1474
1550
|
if (canonicalName === canonicalToolNames.updateStatus) {
|
|
1475
1551
|
if (DESKTOP_MANAGED) {
|
|
1476
1552
|
return {
|
|
@@ -1502,6 +1578,14 @@ class EchoMemMCPServer {
|
|
|
1502
1578
|
? client
|
|
1503
1579
|
: "auto";
|
|
1504
1580
|
const capsuleText = await recomposeCapsuleMarkdown(mode);
|
|
1581
|
+
if (this.client.isSubagentRequest()) {
|
|
1582
|
+
return {
|
|
1583
|
+
content: [{
|
|
1584
|
+
type: "text",
|
|
1585
|
+
text: `${capsuleText}\n\n---\nNot persisted: ${SUBAGENT_DURABLE_WRITE_MESSAGE}`,
|
|
1586
|
+
}],
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1505
1589
|
// If logged in, persist the capsule via passthrough so it's retrievable by contextId.
|
|
1506
1590
|
if (this.client.hasToken()) {
|
|
1507
1591
|
try {
|
|
@@ -1536,6 +1620,8 @@ class EchoMemMCPServer {
|
|
|
1536
1620
|
switch (canonicalName) {
|
|
1537
1621
|
case canonicalToolNames.bindSourceSession:
|
|
1538
1622
|
return await this.handleBindSourceSession(request.params.arguments);
|
|
1623
|
+
case canonicalToolNames.linkWorkspaceTicketSession:
|
|
1624
|
+
return await this.handleLinkWorkspaceTicketSession(request.params.arguments);
|
|
1539
1625
|
case canonicalToolNames.search:
|
|
1540
1626
|
return await this.handleSearch(toolArgs, rec);
|
|
1541
1627
|
case canonicalToolNames.save:
|
|
@@ -1925,6 +2011,37 @@ Details: ${m.details || "N/A"}`)
|
|
|
1925
2011
|
}],
|
|
1926
2012
|
};
|
|
1927
2013
|
}
|
|
2014
|
+
async handleLinkWorkspaceTicketSession(args) {
|
|
2015
|
+
const parsed = linkWorkspaceTicketSessionSchema.parse(args ?? {});
|
|
2016
|
+
const result = await this.client.linkWorkspaceTicketSession(parsed);
|
|
2017
|
+
if (!result) {
|
|
2018
|
+
return {
|
|
2019
|
+
content: [{
|
|
2020
|
+
type: "text",
|
|
2021
|
+
text: [
|
|
2022
|
+
`Ticket ${parsed.ticketId} link is pending verified source-session discovery.`,
|
|
2023
|
+
"Echo Desktop can finish this link from the structured ticket marker or this exact local tool invocation.",
|
|
2024
|
+
"Do not guess a context ID or call bind_source_session on the user's behalf unless its documented compatibility fallback is actually needed.",
|
|
2025
|
+
].join("\n"),
|
|
2026
|
+
}],
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
const ticket = isRecord(result.ticket) ? result.ticket : {};
|
|
2030
|
+
const workspaceId = readString(ticket, "workspaceId") ?? parsed.workspaceId;
|
|
2031
|
+
return {
|
|
2032
|
+
content: [{
|
|
2033
|
+
type: "text",
|
|
2034
|
+
text: [
|
|
2035
|
+
result.changed === true
|
|
2036
|
+
? "Linked this verified source session to the Echo workspace ticket."
|
|
2037
|
+
: "This verified source session was already linked to the Echo workspace ticket.",
|
|
2038
|
+
`Ticket: ${parsed.ticketId}`,
|
|
2039
|
+
workspaceId ? `Workspace: ${workspaceId}` : "",
|
|
2040
|
+
"Retries are safe and do not create duplicate links or history events.",
|
|
2041
|
+
].filter(Boolean).join("\n"),
|
|
2042
|
+
}],
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
1928
2045
|
async handleTimeRange(args) {
|
|
1929
2046
|
const parsed = timeRangeSchema.parse(args ?? {});
|
|
1930
2047
|
const { success, memories, error } = await this.client.getMemoriesByTimeRange(args);
|
package/dist/local-data-paths.js
CHANGED
|
@@ -97,7 +97,7 @@ export function resolveClaudeDesktopSupportRoots(opts = {}) {
|
|
|
97
97
|
if (typeof configured === "string" && configured.trim()) {
|
|
98
98
|
candidates.push(expandCurrentUserHome(configured, homeDir));
|
|
99
99
|
}
|
|
100
|
-
if (platform === "darwin") {
|
|
100
|
+
else if (platform === "darwin") {
|
|
101
101
|
candidates.push(path.join(homeDir, "Library", "Application Support", "Claude"));
|
|
102
102
|
}
|
|
103
103
|
else if (platform === "win32") {
|
|
@@ -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
|
+
}
|
package/dist/migrate.js
CHANGED
|
@@ -320,16 +320,49 @@ function hasClaudeText(content) {
|
|
|
320
320
|
return false;
|
|
321
321
|
return content.some((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* True when this turn opens the conversation rather than continuing one.
|
|
325
|
+
*
|
|
326
|
+
* `parentUuid === null` is the common case, but a session started by dropping in a file opens with
|
|
327
|
+
* an `attachment` record and parents its first real turn to that — requiring a null parent silently
|
|
328
|
+
* discarded those whole transcripts. Walk the chain instead: a turn opens the conversation when
|
|
329
|
+
* nothing conversational precedes it in this file. A parent the file does not contain means the
|
|
330
|
+
* thread continues from somewhere else, which is not a user-created session.
|
|
331
|
+
*/
|
|
332
|
+
function startsConversation(obj, byUuid) {
|
|
333
|
+
let parentUuid = obj.parentUuid;
|
|
334
|
+
for (let depth = 0; depth < 64; depth += 1) {
|
|
335
|
+
if (parentUuid === null || parentUuid === undefined)
|
|
336
|
+
return true;
|
|
337
|
+
if (typeof parentUuid !== "string")
|
|
338
|
+
return false;
|
|
339
|
+
const parent = byUuid.get(parentUuid);
|
|
340
|
+
if (!parent)
|
|
341
|
+
return false;
|
|
342
|
+
if (parent.type === "user" || parent.type === "assistant")
|
|
343
|
+
return false;
|
|
344
|
+
parentUuid = parent.parentUuid;
|
|
345
|
+
}
|
|
346
|
+
return false; // pathological chain; treat as not session-initiating rather than loop
|
|
347
|
+
}
|
|
323
348
|
function isUserCreatedClaudeSessionFile(file) {
|
|
324
|
-
|
|
325
|
-
|
|
349
|
+
const objects = initialJsonObjects(file).filter(isRecord);
|
|
350
|
+
const byUuid = new Map();
|
|
351
|
+
for (const obj of objects) {
|
|
352
|
+
if (typeof obj.uuid === "string" && obj.uuid)
|
|
353
|
+
byUuid.set(obj.uuid, obj);
|
|
354
|
+
}
|
|
355
|
+
return objects.some((obj) => {
|
|
356
|
+
if (obj.type !== "user")
|
|
326
357
|
return false;
|
|
327
|
-
if (obj.userType !== "external" || obj.isSidechain !== false
|
|
358
|
+
if (obj.userType !== "external" || obj.isSidechain !== false)
|
|
328
359
|
return false;
|
|
329
360
|
if (obj.isMeta === true || obj.isCompactSummary === true)
|
|
330
361
|
return false;
|
|
331
362
|
if (typeof obj.agentId === "string" && obj.agentId.trim())
|
|
332
363
|
return false;
|
|
364
|
+
if (!startsConversation(obj, byUuid))
|
|
365
|
+
return false;
|
|
333
366
|
const message = isRecord(obj.message) ? obj.message : {};
|
|
334
367
|
return hasClaudeText(message.content);
|
|
335
368
|
});
|