@echomem/mcp 1.4.52 → 1.4.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.js +52 -18
- package/dist/source-session.js +32 -3
- package/dist/v1-contract.js +27 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,6 +133,20 @@ Both hook entries are merged idempotently into `~/.codex/hooks.json` and
|
|
|
133
133
|
`~/.claude/settings.json`; unrelated user hooks and settings are preserved. Hooks fail open when
|
|
134
134
|
their local input is missing so they cannot prevent a conversation from starting or finishing.
|
|
135
135
|
|
|
136
|
+
### Cloud Cowork through `remote-devices`
|
|
137
|
+
|
|
138
|
+
Cloud Cowork inference cannot use the local hook or transcript-verification path. The shared local
|
|
139
|
+
MCP process also must not retain one cloud conversation as global bridge state. Cloud Cowork agents
|
|
140
|
+
therefore pass their runtime-issued `session_...` identifier as `cloudCoworkSessionId` on each
|
|
141
|
+
EchoMem tool call. EchoMem binds that request to the canonical `cowork:session_...` context with
|
|
142
|
+
`cloud_cowork_tool_argument` evidence and `remote_mcp` origin, then links or saves against that
|
|
143
|
+
context without changing shared bridge state.
|
|
144
|
+
|
|
145
|
+
This identity is intentionally reported as **provisional**: the authenticated user owns the
|
|
146
|
+
context and ticket link, but EchoMem cannot independently corroborate the cloud identifier from a
|
|
147
|
+
local transcript. Verified local Cowork remains `cowork:local_...` with trusted local evidence.
|
|
148
|
+
Never convert one form into the other or silently present a provisional cloud link as verified.
|
|
149
|
+
|
|
136
150
|
### Manual / headless (SSH, containers, CI)
|
|
137
151
|
|
|
138
152
|
No browser? Provide secrets directly — this is the documented headless path:
|
package/dist/index.js
CHANGED
|
@@ -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 { resolveSourceSessionRequestContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
|
|
19
|
+
import { SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE, resolveCloudCoworkSessionFromToolArguments, 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(/\/$/, "");
|
|
@@ -711,7 +711,7 @@ class EchoMemApiClient {
|
|
|
711
711
|
const requestContext = this.requestContext.getStore();
|
|
712
712
|
return requestContext === undefined ? this.boundSourceSession : requestContext.sourceSession;
|
|
713
713
|
}
|
|
714
|
-
|
|
714
|
+
getRequestSourceSession() {
|
|
715
715
|
const requestContext = this.requestContext.getStore();
|
|
716
716
|
if (!requestContext || requestContext.lineageStatus === "unbound")
|
|
717
717
|
return null;
|
|
@@ -723,26 +723,29 @@ class EchoMemApiClient {
|
|
|
723
723
|
async withRequestContext(requestContext, operation) {
|
|
724
724
|
return this.requestContext.run(requestContext, operation);
|
|
725
725
|
}
|
|
726
|
-
async bindSourceSession(
|
|
726
|
+
async bindSourceSession(identity, persistForBridge = true) {
|
|
727
727
|
this.synchronizeAccountContext();
|
|
728
|
-
|
|
728
|
+
if (identity.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE && persistForBridge) {
|
|
729
|
+
throw new Error("A provisional Cloud Cowork identity cannot become shared bridge state");
|
|
730
|
+
}
|
|
731
|
+
const cached = this.sourceSessionsByCanonicalKey.get(identity.canonicalKey);
|
|
729
732
|
if (cached) {
|
|
730
733
|
if (persistForBridge)
|
|
731
734
|
this.boundSourceSession = cached;
|
|
732
735
|
return { ...cached, created: false };
|
|
733
736
|
}
|
|
734
737
|
const response = await this.axios.post("/api/extension/source-sessions/bind", {
|
|
735
|
-
provider:
|
|
736
|
-
providerSessionId:
|
|
737
|
-
evidence:
|
|
738
|
+
provider: identity.provider,
|
|
739
|
+
providerSessionId: identity.providerSessionId,
|
|
740
|
+
evidence: identity.evidence,
|
|
738
741
|
});
|
|
739
742
|
const contextId = readString(response.data, "contextId");
|
|
740
743
|
const canonicalKey = readString(response.data, "canonicalKey");
|
|
741
|
-
if (response.data?.success !== true || !contextId || canonicalKey !==
|
|
744
|
+
if (response.data?.success !== true || !contextId || canonicalKey !== identity.canonicalKey) {
|
|
742
745
|
throw new Error("EchoMem returned an invalid source-session binding receipt");
|
|
743
746
|
}
|
|
744
|
-
const bound = { ...
|
|
745
|
-
this.sourceSessionsByCanonicalKey.set(
|
|
747
|
+
const bound = { ...identity, contextId };
|
|
748
|
+
this.sourceSessionsByCanonicalKey.set(identity.canonicalKey, bound);
|
|
746
749
|
if (this.sourceSessionsByCanonicalKey.size > 64) {
|
|
747
750
|
const oldestKey = this.sourceSessionsByCanonicalKey.keys().next().value;
|
|
748
751
|
if (oldestKey)
|
|
@@ -759,9 +762,10 @@ class EchoMemApiClient {
|
|
|
759
762
|
const parsed = linkWorkspaceTicketSessionSchema.parse(args ?? {});
|
|
760
763
|
// A bridge-level compatibility binding can outlive a conversation in
|
|
761
764
|
// long-running hosts. Ticket links are therefore allowed to take the fast
|
|
762
|
-
// path only from identity
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
+
// path only from identity scoped to this exact request. That identity is
|
|
766
|
+
// either verified host metadata or an explicitly marked provisional Cloud
|
|
767
|
+
// Cowork claim; otherwise Desktop completes the link from local evidence.
|
|
768
|
+
const sourceSession = this.getRequestSourceSession();
|
|
765
769
|
if (!sourceSession)
|
|
766
770
|
return null;
|
|
767
771
|
const response = await this.axios.post(`/api/extension/workspace-tickets/${encodeURIComponent(parsed.ticketId)}/sessions`, {
|
|
@@ -1040,10 +1044,14 @@ class EchoMemApiClient {
|
|
|
1040
1044
|
// For an encrypted account, hand the server the key transiently in the X-Encryption-Key header
|
|
1041
1045
|
// so it encrypts at rest (mirrors the extension's write path, spec §3.1a). Locked → LockedError.
|
|
1042
1046
|
const enc = await this.encState();
|
|
1047
|
+
const boundSourceSession = this.getBoundSourceSession();
|
|
1048
|
+
const originChannel = boundSourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE
|
|
1049
|
+
? "remote_mcp"
|
|
1050
|
+
: "local_mcp";
|
|
1043
1051
|
const config = {
|
|
1044
1052
|
headers: {
|
|
1045
1053
|
"X-EchoMem-Request-Id": randomUUID(),
|
|
1046
|
-
"X-EchoMem-Origin-Channel":
|
|
1054
|
+
"X-EchoMem-Origin-Channel": originChannel,
|
|
1047
1055
|
...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
|
|
1048
1056
|
},
|
|
1049
1057
|
};
|
|
@@ -1053,9 +1061,10 @@ class EchoMemApiClient {
|
|
|
1053
1061
|
sourceUrl: parsed.url,
|
|
1054
1062
|
source: parsed.source || "mcp_server",
|
|
1055
1063
|
title: parsed.title,
|
|
1056
|
-
// A
|
|
1064
|
+
// A source-session identity owns the memory context. Provisional Cloud Cowork identity
|
|
1065
|
+
// is explicitly marked by its evidence and origin channel. The independent opaque scope still
|
|
1057
1066
|
// governs group-sharing consent and never becomes the source-session identity.
|
|
1058
|
-
conversationKey:
|
|
1067
|
+
conversationKey: boundSourceSession?.canonicalKey ?? groupSharingScopeId,
|
|
1059
1068
|
groupSharingScopeId,
|
|
1060
1069
|
passthrough: parsed.passthrough || false,
|
|
1061
1070
|
triggerMessage: parsed.triggerMessage ||
|
|
@@ -1502,6 +1511,23 @@ class EchoMemMCPServer {
|
|
|
1502
1511
|
catch {
|
|
1503
1512
|
// Malformed host metadata is not model input. Ignore it and retain the explicit fallback.
|
|
1504
1513
|
}
|
|
1514
|
+
let provisionalCloudSession = null;
|
|
1515
|
+
try {
|
|
1516
|
+
provisionalCloudSession = resolveCloudCoworkSessionFromToolArguments(toolArgs);
|
|
1517
|
+
}
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : "Invalid Cloud Cowork session identity");
|
|
1520
|
+
}
|
|
1521
|
+
if (provisionalCloudSession && sourceResolution.sourceSession) {
|
|
1522
|
+
throw new McpError(ErrorCode.InvalidParams, "This request already has a verified local source session. Omit cloudCoworkSessionId rather than mixing identities.");
|
|
1523
|
+
}
|
|
1524
|
+
if (provisionalCloudSession) {
|
|
1525
|
+
sourceResolution = {
|
|
1526
|
+
sourceSession: provisionalCloudSession,
|
|
1527
|
+
isSubagent: false,
|
|
1528
|
+
lineageStatus: "not_applicable",
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1505
1531
|
// A child request starts fail-closed. Resolved children bind to the originating root; unresolved
|
|
1506
1532
|
// children carry an explicit null so AsyncLocalStorage cannot fall through to a global binding.
|
|
1507
1533
|
let requestSourceSession = sourceResolution.isSubagent
|
|
@@ -2052,17 +2078,25 @@ Details: ${m.details || "N/A"}`)
|
|
|
2052
2078
|
const ticket = isRecord(result.ticket) ? result.ticket : {};
|
|
2053
2079
|
const workspaceId = readString(ticket, "workspaceId") ?? parsed.workspaceId;
|
|
2054
2080
|
const sourceSession = this.client.getBoundSourceSession();
|
|
2081
|
+
const provisional = sourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE;
|
|
2055
2082
|
return {
|
|
2056
2083
|
content: [{
|
|
2057
2084
|
type: "text",
|
|
2058
2085
|
text: [
|
|
2059
2086
|
result.changed === true
|
|
2060
|
-
?
|
|
2061
|
-
|
|
2087
|
+
? provisional
|
|
2088
|
+
? "Linked this provisional Cloud Cowork session to the Echo workspace ticket."
|
|
2089
|
+
: "Linked this verified source session to the Echo workspace ticket."
|
|
2090
|
+
: provisional
|
|
2091
|
+
? "This provisional Cloud Cowork session was already linked to the Echo workspace ticket."
|
|
2092
|
+
: "This verified source session was already linked to the Echo workspace ticket.",
|
|
2062
2093
|
`Ticket: ${parsed.ticketId}`,
|
|
2063
2094
|
workspaceId ? `Workspace: ${workspaceId}` : "",
|
|
2064
2095
|
sourceSession ? `Source session: ${sourceSession.canonicalKey}` : "",
|
|
2065
2096
|
sourceSession ? `Context: ${sourceSession.contextId}` : "",
|
|
2097
|
+
provisional
|
|
2098
|
+
? "Identity trust: provisional (reported by Cloud Cowork in the tool call; not verified by a local transcript). Repeat cloudCoworkSessionId on later EchoMem calls from this conversation."
|
|
2099
|
+
: "",
|
|
2066
2100
|
"Retries are safe and do not create duplicate links or history events.",
|
|
2067
2101
|
].filter(Boolean).join("\n"),
|
|
2068
2102
|
}],
|
package/dist/source-session.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveClaudeCoworkSessionsDir, resolveClaudeProjectsDir, resolveCodexS
|
|
|
4
4
|
export const SOURCE_SESSION_BINDING_EVIDENCE = "local_jsonl_tool_call";
|
|
5
5
|
export const SOURCE_SESSION_HOOK_EVIDENCE = "local_session_start_hook";
|
|
6
6
|
export const SOURCE_SESSION_MCP_METADATA_EVIDENCE = "local_mcp_session_metadata";
|
|
7
|
+
export const SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE = "cloud_cowork_tool_argument";
|
|
7
8
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
9
|
const UUID_IN_TEXT_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
9
10
|
const MAX_TAIL_BYTES = 512 * 1024;
|
|
@@ -20,13 +21,18 @@ function normalizedProviderSessionId(provider, value) {
|
|
|
20
21
|
if (provider === "codex" && !UUID_RE.test(normalized)) {
|
|
21
22
|
throw new Error("Codex source-session ID must be a UUID");
|
|
22
23
|
}
|
|
23
|
-
if (provider === "cowork"
|
|
24
|
-
|
|
24
|
+
if (provider === "cowork"
|
|
25
|
+
&& !/^local_[A-Za-z0-9_-]{1,194}$/.test(normalized)
|
|
26
|
+
&& !/^session_[A-Za-z0-9_-]{1,192}$/.test(normalized)) {
|
|
27
|
+
throw new Error("Cowork source-session ID must use a local_ host ID or session_ Cloud Cowork ID");
|
|
25
28
|
}
|
|
26
29
|
return UUID_RE.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
27
30
|
}
|
|
28
31
|
export function verifiedSourceSession(provider, providerSessionId, evidence) {
|
|
29
32
|
const normalized = normalizedProviderSessionId(provider, providerSessionId);
|
|
33
|
+
if (provider === "cowork" && !normalized.startsWith("local_")) {
|
|
34
|
+
throw new Error("Verified Cowork source-session ID must use the local_ host identifier");
|
|
35
|
+
}
|
|
30
36
|
return {
|
|
31
37
|
provider,
|
|
32
38
|
providerSessionId: normalized,
|
|
@@ -34,6 +40,26 @@ export function verifiedSourceSession(provider, providerSessionId, evidence) {
|
|
|
34
40
|
evidence,
|
|
35
41
|
};
|
|
36
42
|
}
|
|
43
|
+
export function provisionalCloudCoworkSession(providerSessionId) {
|
|
44
|
+
const normalized = normalizedProviderSessionId("cowork", providerSessionId);
|
|
45
|
+
if (!normalized.startsWith("session_")) {
|
|
46
|
+
throw new Error("Cloud Cowork source-session ID must use the session_ identifier from the cloud runtime");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
provider: "cowork",
|
|
50
|
+
providerSessionId: normalized,
|
|
51
|
+
canonicalKey: `cowork:${normalized}`,
|
|
52
|
+
evidence: SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function resolveCloudCoworkSessionFromToolArguments(args) {
|
|
56
|
+
if (!isRecord(args) || args.cloudCoworkSessionId === undefined)
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof args.cloudCoworkSessionId !== "string") {
|
|
59
|
+
throw new Error("cloudCoworkSessionId must be the current Cloud Cowork session_ identifier");
|
|
60
|
+
}
|
|
61
|
+
return provisionalCloudCoworkSession(args.cloudCoworkSessionId);
|
|
62
|
+
}
|
|
37
63
|
function isRecord(value) {
|
|
38
64
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
65
|
}
|
|
@@ -393,7 +419,10 @@ export function resolveSourceSessionRequestContext(metadata, options = {}) {
|
|
|
393
419
|
return { sourceSession: null, isSubagent: false, lineageStatus: "unbound" };
|
|
394
420
|
}
|
|
395
421
|
export function resolveSourceSessionFromMcpContext(metadata, options = {}) {
|
|
396
|
-
|
|
422
|
+
const sourceSession = resolveSourceSessionRequestContext(metadata, options).sourceSession;
|
|
423
|
+
return sourceSession?.evidence === SOURCE_SESSION_CLOUD_TOOL_ARGUMENT_EVIDENCE
|
|
424
|
+
? null
|
|
425
|
+
: sourceSession;
|
|
397
426
|
}
|
|
398
427
|
export function resolveSourceSessionFromBindingToken(bindingToken, options = {}) {
|
|
399
428
|
const normalizedToken = bindingToken.trim().toLowerCase();
|
package/dist/v1-contract.js
CHANGED
|
@@ -164,7 +164,32 @@ function decorateLocalToolSpec(tool) {
|
|
|
164
164
|
const triggerMetadataSchema = {
|
|
165
165
|
triggerMessage: z.string().optional(),
|
|
166
166
|
triggerMessageRole: z.string().optional(),
|
|
167
|
+
cloudCoworkSessionId: z.string().regex(/^session_[A-Za-z0-9_-]{1,192}$/).optional(),
|
|
167
168
|
};
|
|
169
|
+
const CLOUD_COWORK_SESSION_DESCRIPTION = "Cloud Cowork only: the current session_ identifier supplied by the Cowork runtime. Repeat it on each EchoMem call so the shared remote-devices bridge can attach that one request to the same provisional conversation context. Never guess or reuse an ID from another conversation. Local Cowork, Claude Code, and Codex must omit this field.";
|
|
170
|
+
const CLOUD_COWORK_SESSION_PROPERTY = {
|
|
171
|
+
type: "string",
|
|
172
|
+
pattern: "^session_[A-Za-z0-9_-]{1,192}$",
|
|
173
|
+
description: CLOUD_COWORK_SESSION_DESCRIPTION,
|
|
174
|
+
};
|
|
175
|
+
const SOURCE_CONTEXT_FREE_TOOL_NAMES = new Set([
|
|
176
|
+
canonicalToolNames.bindSourceSession,
|
|
177
|
+
canonicalToolNames.updateStatus,
|
|
178
|
+
canonicalToolNames.contextHealth,
|
|
179
|
+
canonicalToolNames.recompose,
|
|
180
|
+
]);
|
|
181
|
+
function injectCloudCoworkSession(specs) {
|
|
182
|
+
for (const spec of specs) {
|
|
183
|
+
if (SOURCE_CONTEXT_FREE_TOOL_NAMES.has(spec.name))
|
|
184
|
+
continue;
|
|
185
|
+
const existing = spec.inputSchema.properties ?? {};
|
|
186
|
+
spec.inputSchema.properties = {
|
|
187
|
+
...existing,
|
|
188
|
+
cloudCoworkSessionId: CLOUD_COWORK_SESSION_PROPERTY,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return specs;
|
|
192
|
+
}
|
|
168
193
|
// Canonical workspace selector for tools that act inside a company workspace.
|
|
169
194
|
// `workspaceId` is the current name; `groupId` is the legacy alias kept working
|
|
170
195
|
// so existing prompts keep functioning. Handlers normalize with
|
|
@@ -478,7 +503,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
478
503
|
{
|
|
479
504
|
name: canonicalToolNames.linkWorkspaceTicketSession,
|
|
480
505
|
title: "Link this agent conversation to an Echo workspace ticket",
|
|
481
|
-
description: withMcpVersion("Call when the user asks to work on, continue, attach, or bind this conversation to an Echo workspace ticket and supplies its UUID. Pass the ticketId exactly; pass workspaceId when a structured Echo ticket marker provides it. The operation is idempotent.
|
|
506
|
+
description: withMcpVersion("Call when the user asks to work on, continue, attach, or bind this conversation to an Echo workspace ticket and supplies its UUID. Pass the ticketId exactly; pass workspaceId when a structured Echo ticket marker provides it. The operation is idempotent. A verified local source session links immediately. Cloud Cowork has no local transcript, so pass its current session_ runtime identifier as cloudCoworkSessionId; EchoMem records that path explicitly as provisional and links it immediately. Otherwise Echo Desktop can recover this exact invocation from a local transcript. Never guess or ask the user for a source-session/context ID."),
|
|
482
507
|
inputSchema: {
|
|
483
508
|
type: "object",
|
|
484
509
|
properties: {
|
|
@@ -1097,5 +1122,5 @@ export function listToolSpecs(opts = {}) {
|
|
|
1097
1122
|
},
|
|
1098
1123
|
},
|
|
1099
1124
|
];
|
|
1100
|
-
return injectWorkspaceSelector(tools).map(decorateLocalToolSpec);
|
|
1125
|
+
return injectWorkspaceSelector(injectCloudCoworkSession(tools)).map(decorateLocalToolSpec);
|
|
1101
1126
|
}
|