@echomem/mcp 1.4.50 → 1.4.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/config-files.js +63 -0
- package/dist/headless-runtime.js +59 -8
- package/dist/hud/hooks.js +85 -31
- package/dist/index.js +129 -12
- package/dist/mcp-control.js +215 -0
- package/dist/setup.js +410 -27
- package/dist/source-session.js +253 -75
- package/dist/v1-contract.js +34 -0
- package/package.json +4 -2
package/dist/source-session.js
CHANGED
|
@@ -9,6 +9,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
|
|
|
9
9
|
const MAX_TAIL_BYTES = 512 * 1024;
|
|
10
10
|
const DEFAULT_MAX_AGE_MS = 15 * 60 * 1000;
|
|
11
11
|
const MAX_RECENT_FILES_PER_PROVIDER = 24;
|
|
12
|
+
const MAX_CODEX_AGENT_DEPTH = 16;
|
|
13
|
+
const CODEX_FILE_INDEX_TTL_MS = 5_000;
|
|
14
|
+
const codexFileIndexes = new Map();
|
|
12
15
|
function normalizedProviderSessionId(provider, value) {
|
|
13
16
|
const normalized = value.trim();
|
|
14
17
|
if (!normalized || normalized.length > 200 || /\s/.test(normalized)) {
|
|
@@ -31,47 +34,73 @@ export function verifiedSourceSession(provider, providerSessionId, evidence) {
|
|
|
31
34
|
evidence,
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function normalizedUuid(value) {
|
|
41
|
+
if (typeof value !== "string")
|
|
42
|
+
return null;
|
|
43
|
+
const normalized = value.trim().toLowerCase();
|
|
44
|
+
return UUID_RE.test(normalized) ? normalized : null;
|
|
45
|
+
}
|
|
46
|
+
function sourceParentThreadId(source) {
|
|
47
|
+
if (!isRecord(source) || !isRecord(source.subagent))
|
|
48
|
+
return null;
|
|
49
|
+
const direct = normalizedUuid(source.subagent.parent_thread_id);
|
|
50
|
+
if (direct)
|
|
51
|
+
return direct;
|
|
52
|
+
const spawn = isRecord(source.subagent.thread_spawn) ? source.subagent.thread_spawn : null;
|
|
53
|
+
return normalizedUuid(spawn?.parent_thread_id);
|
|
54
|
+
}
|
|
55
|
+
function metadataMarksSubagent(value) {
|
|
56
|
+
if (value == null || value === false || value === 0)
|
|
57
|
+
return false;
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
const normalized = value.trim().toLowerCase();
|
|
60
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
|
|
57
61
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function codexLineageNode(file) {
|
|
65
|
+
let descriptor = null;
|
|
66
|
+
try {
|
|
67
|
+
descriptor = fs.openSync(file, "r");
|
|
68
|
+
const stat = fs.fstatSync(descriptor);
|
|
69
|
+
const bytes = Math.min(stat.size, 128 * 1024);
|
|
70
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
71
|
+
fs.readSync(descriptor, buffer, 0, bytes, 0);
|
|
72
|
+
for (const line of buffer.toString("utf8").split(/\r?\n/)) {
|
|
73
|
+
if (!line.includes("session_meta"))
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const row = JSON.parse(line);
|
|
77
|
+
if (!isRecord(row) || row.type !== "session_meta" || !isRecord(row.payload))
|
|
78
|
+
continue;
|
|
79
|
+
const id = normalizedUuid(row.payload.id) ?? normalizedUuid(row.payload.session_id);
|
|
80
|
+
if (!id)
|
|
81
|
+
return null;
|
|
82
|
+
const source = row.payload.source;
|
|
83
|
+
const directParent = normalizedUuid(row.payload.parent_thread_id);
|
|
84
|
+
const parentThreadId = directParent ?? sourceParentThreadId(source);
|
|
85
|
+
const isSubagent = parentThreadId !== null
|
|
86
|
+
|| (isRecord(source) && Object.hasOwn(source, "subagent"))
|
|
87
|
+
|| row.payload.thread_source === "subagent";
|
|
88
|
+
return { id, isSubagent, parentThreadId };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
63
94
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
if (descriptor !== null)
|
|
100
|
+
fs.closeSync(descriptor);
|
|
69
101
|
}
|
|
70
102
|
return null;
|
|
71
103
|
}
|
|
72
|
-
function isRecord(value) {
|
|
73
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
-
}
|
|
75
104
|
function readableDirectory(candidate) {
|
|
76
105
|
try {
|
|
77
106
|
return fs.statSync(candidate).isDirectory();
|
|
@@ -115,8 +144,105 @@ function walkJsonlFiles(roots) {
|
|
|
115
144
|
}
|
|
116
145
|
return files;
|
|
117
146
|
}
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
/** Codex stores spawned-agent transcripts below `subagents`, so lineage discovery must include them. */
|
|
148
|
+
function walkCodexJsonlFiles(roots) {
|
|
149
|
+
const files = [];
|
|
150
|
+
const pending = roots.filter(readableDirectory);
|
|
151
|
+
while (pending.length) {
|
|
152
|
+
const directory = pending.pop();
|
|
153
|
+
let entries;
|
|
154
|
+
try {
|
|
155
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const target = path.join(directory, entry.name);
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
pending.push(target);
|
|
164
|
+
}
|
|
165
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
166
|
+
files.push(target);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return files;
|
|
171
|
+
}
|
|
172
|
+
function codexFileIndexKey(roots) {
|
|
173
|
+
return [...roots].map((root) => path.resolve(root)).sort().join("\u0000");
|
|
174
|
+
}
|
|
175
|
+
function buildCodexFileIndex(roots) {
|
|
176
|
+
const filesById = new Map();
|
|
177
|
+
for (const file of walkCodexJsonlFiles(roots)) {
|
|
178
|
+
const filenameId = path.basename(file).match(UUID_IN_TEXT_RE)?.[0]?.toLowerCase();
|
|
179
|
+
if (filenameId && !filesById.has(filenameId))
|
|
180
|
+
filesById.set(filenameId, file);
|
|
181
|
+
}
|
|
182
|
+
return filesById;
|
|
183
|
+
}
|
|
184
|
+
function codexFileForSession(sessionId, roots, forceRefresh = false) {
|
|
185
|
+
const key = codexFileIndexKey(roots);
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
let cached = codexFileIndexes.get(key);
|
|
188
|
+
if (forceRefresh || !cached || cached.expiresAtMs <= now) {
|
|
189
|
+
cached = {
|
|
190
|
+
expiresAtMs: now + CODEX_FILE_INDEX_TTL_MS,
|
|
191
|
+
filesById: buildCodexFileIndex(roots),
|
|
192
|
+
};
|
|
193
|
+
codexFileIndexes.set(key, cached);
|
|
194
|
+
}
|
|
195
|
+
const normalized = sessionId.toLowerCase();
|
|
196
|
+
return cached.filesById.get(normalized) ?? null;
|
|
197
|
+
}
|
|
198
|
+
function codexNodeForSession(sessionId, roots) {
|
|
199
|
+
let file = codexFileForSession(sessionId, roots);
|
|
200
|
+
if (!file)
|
|
201
|
+
file = codexFileForSession(sessionId, roots, true);
|
|
202
|
+
if (!file)
|
|
203
|
+
return null;
|
|
204
|
+
const node = codexLineageNode(file);
|
|
205
|
+
return node?.id === sessionId.toLowerCase() ? node : null;
|
|
206
|
+
}
|
|
207
|
+
function resolveCodexRootThread(agentThreadId, explicitParentThreadId, explicitSubagentMarker, roots) {
|
|
208
|
+
const localAgent = codexNodeForSession(agentThreadId, roots);
|
|
209
|
+
const isSubagent = explicitSubagentMarker || explicitParentThreadId !== null || localAgent?.isSubagent === true;
|
|
210
|
+
if (!isSubagent) {
|
|
211
|
+
return { isSubagent: false, rootThreadId: agentThreadId, parentThreadId: null, agentDepth: 0 };
|
|
212
|
+
}
|
|
213
|
+
const localParent = localAgent?.parentThreadId ?? null;
|
|
214
|
+
if (explicitParentThreadId && localParent && explicitParentThreadId !== localParent) {
|
|
215
|
+
return { isSubagent: true, rootThreadId: null, parentThreadId: explicitParentThreadId, agentDepth: 0 };
|
|
216
|
+
}
|
|
217
|
+
const firstParent = explicitParentThreadId ?? localParent;
|
|
218
|
+
if (!firstParent) {
|
|
219
|
+
return { isSubagent: true, rootThreadId: null, parentThreadId: null, agentDepth: 0 };
|
|
220
|
+
}
|
|
221
|
+
const visited = new Set([agentThreadId]);
|
|
222
|
+
let currentId = firstParent;
|
|
223
|
+
let depth = 1;
|
|
224
|
+
while (depth <= MAX_CODEX_AGENT_DEPTH) {
|
|
225
|
+
if (visited.has(currentId)) {
|
|
226
|
+
return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
|
|
227
|
+
}
|
|
228
|
+
visited.add(currentId);
|
|
229
|
+
const current = codexNodeForSession(currentId, roots);
|
|
230
|
+
if (!current) {
|
|
231
|
+
return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
|
|
232
|
+
}
|
|
233
|
+
if (!current.isSubagent) {
|
|
234
|
+
return { isSubagent: true, rootThreadId: current.id, parentThreadId: firstParent, agentDepth: depth };
|
|
235
|
+
}
|
|
236
|
+
if (!current.parentThreadId) {
|
|
237
|
+
return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
|
|
238
|
+
}
|
|
239
|
+
currentId = current.parentThreadId;
|
|
240
|
+
depth += 1;
|
|
241
|
+
}
|
|
242
|
+
return { isSubagent: true, rootThreadId: null, parentThreadId: firstParent, agentDepth: depth };
|
|
243
|
+
}
|
|
244
|
+
function recentFiles(roots, nowMs, maxAgeMs, includeCodexSubagents = false) {
|
|
245
|
+
return (includeCodexSubagents ? walkCodexJsonlFiles(roots) : walkJsonlFiles(roots))
|
|
120
246
|
.map((file) => {
|
|
121
247
|
try {
|
|
122
248
|
return { file, mtimeMs: fs.statSync(file).mtimeMs };
|
|
@@ -151,41 +277,11 @@ function tailContainsBinding(file, bindingToken) {
|
|
|
151
277
|
fs.closeSync(descriptor);
|
|
152
278
|
}
|
|
153
279
|
}
|
|
154
|
-
function codexSessionId(file) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const bytes = Math.min(stat.size, 128 * 1024);
|
|
160
|
-
const buffer = Buffer.allocUnsafe(bytes);
|
|
161
|
-
fs.readSync(descriptor, buffer, 0, bytes, 0);
|
|
162
|
-
for (const line of buffer.toString("utf8").split(/\r?\n/)) {
|
|
163
|
-
if (!line.includes("session_meta"))
|
|
164
|
-
continue;
|
|
165
|
-
try {
|
|
166
|
-
const row = JSON.parse(line);
|
|
167
|
-
if (!isRecord(row) || row.type !== "session_meta" || !isRecord(row.payload))
|
|
168
|
-
continue;
|
|
169
|
-
if (typeof row.payload.parent_thread_id === "string" && row.payload.parent_thread_id)
|
|
170
|
-
return null;
|
|
171
|
-
const raw = typeof row.payload.id === "string"
|
|
172
|
-
? row.payload.id
|
|
173
|
-
: typeof row.payload.session_id === "string"
|
|
174
|
-
? row.payload.session_id
|
|
175
|
-
: "";
|
|
176
|
-
return UUID_RE.test(raw) ? raw.toLowerCase() : null;
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
finally {
|
|
187
|
-
if (descriptor !== null)
|
|
188
|
-
fs.closeSync(descriptor);
|
|
280
|
+
function codexSessionId(file, roots) {
|
|
281
|
+
const node = codexLineageNode(file);
|
|
282
|
+
if (node) {
|
|
283
|
+
const resolution = resolveCodexRootThread(node.id, node.parentThreadId, node.isSubagent, roots);
|
|
284
|
+
return resolution.rootThreadId;
|
|
189
285
|
}
|
|
190
286
|
return path.basename(file).match(UUID_IN_TEXT_RE)?.[0]?.toLowerCase() ?? null;
|
|
191
287
|
}
|
|
@@ -208,6 +304,88 @@ function enabledProviders(hostPlatform) {
|
|
|
208
304
|
}
|
|
209
305
|
return ["codex", "claude-code", "cowork"];
|
|
210
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Resolve host-owned request metadata into EchoMem's root human-thread context. A Codex subagent
|
|
309
|
+
* gets its own model/tool transcript, but its durable memory context remains the originating root.
|
|
310
|
+
* Child lineage must be corroborated by local host JSONL; failures stay explicitly unbound.
|
|
311
|
+
*/
|
|
312
|
+
export function resolveSourceSessionRequestContext(metadata, options = {}) {
|
|
313
|
+
const host = options.hostPlatform?.toLowerCase().replace(/[^a-z0-9]+/g, "_") ?? "";
|
|
314
|
+
const env = options.env ?? process.env;
|
|
315
|
+
if (host.includes("codex")) {
|
|
316
|
+
const root = isRecord(metadata) ? metadata : {};
|
|
317
|
+
const turn = isRecord(root["x-codex-turn-metadata"])
|
|
318
|
+
? root["x-codex-turn-metadata"]
|
|
319
|
+
: {};
|
|
320
|
+
const agentThreadId = normalizedUuid(turn.thread_id)
|
|
321
|
+
?? normalizedUuid(turn.session_id)
|
|
322
|
+
?? normalizedUuid(root.threadId);
|
|
323
|
+
const parentThreadId = normalizedUuid(root["x-codex-parent-thread-id"])
|
|
324
|
+
?? normalizedUuid(turn.parent_thread_id)
|
|
325
|
+
?? normalizedUuid(turn.parentThreadId)
|
|
326
|
+
?? sourceParentThreadId(turn.source);
|
|
327
|
+
const explicitSubagentMarker = parentThreadId !== null
|
|
328
|
+
|| metadataMarksSubagent(root["x-openai-subagent"])
|
|
329
|
+
|| turn.thread_source === "subagent"
|
|
330
|
+
|| metadataMarksSubagent(turn.subagent_kind)
|
|
331
|
+
|| (isRecord(turn.source) && Object.hasOwn(turn.source, "subagent"));
|
|
332
|
+
if (!agentThreadId) {
|
|
333
|
+
return {
|
|
334
|
+
sourceSession: null,
|
|
335
|
+
isSubagent: explicitSubagentMarker,
|
|
336
|
+
lineageStatus: explicitSubagentMarker ? "unresolved_subagent" : "unbound",
|
|
337
|
+
parentThreadId: parentThreadId ?? undefined,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const codexRoots = options.roots?.codex
|
|
341
|
+
?? resolveCodexSessionRoots({ env }).map((entry) => entry.path);
|
|
342
|
+
const lineage = resolveCodexRootThread(agentThreadId, parentThreadId, explicitSubagentMarker, codexRoots);
|
|
343
|
+
if (lineage.isSubagent && !lineage.rootThreadId) {
|
|
344
|
+
return {
|
|
345
|
+
sourceSession: null,
|
|
346
|
+
isSubagent: true,
|
|
347
|
+
lineageStatus: "unresolved_subagent",
|
|
348
|
+
agentThreadId,
|
|
349
|
+
parentThreadId: lineage.parentThreadId ?? undefined,
|
|
350
|
+
agentDepth: lineage.agentDepth,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const rootThreadId = lineage.rootThreadId ?? agentThreadId;
|
|
354
|
+
return {
|
|
355
|
+
sourceSession: verifiedSourceSession("codex", rootThreadId, SOURCE_SESSION_MCP_METADATA_EVIDENCE),
|
|
356
|
+
isSubagent: lineage.isSubagent,
|
|
357
|
+
lineageStatus: lineage.isSubagent ? "resolved_subagent" : "top_level",
|
|
358
|
+
agentThreadId,
|
|
359
|
+
parentThreadId: lineage.parentThreadId ?? undefined,
|
|
360
|
+
rootThreadId,
|
|
361
|
+
agentDepth: lineage.agentDepth,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (host.includes("claude_desktop") || host === "claude" || host.includes("cowork")) {
|
|
365
|
+
const candidate = env.CLAUDE_CODE_HOST_SESSION_ID?.trim() ?? "";
|
|
366
|
+
return {
|
|
367
|
+
sourceSession: candidate
|
|
368
|
+
? verifiedSourceSession("cowork", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
|
|
369
|
+
: null,
|
|
370
|
+
isSubagent: false,
|
|
371
|
+
lineageStatus: candidate ? "not_applicable" : "unbound",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (host.includes("claude_code")) {
|
|
375
|
+
const candidate = env.CLAUDE_CODE_SESSION_ID?.trim() ?? "";
|
|
376
|
+
return {
|
|
377
|
+
sourceSession: candidate
|
|
378
|
+
? verifiedSourceSession("claude-code", candidate, SOURCE_SESSION_MCP_METADATA_EVIDENCE)
|
|
379
|
+
: null,
|
|
380
|
+
isSubagent: false,
|
|
381
|
+
lineageStatus: candidate ? "not_applicable" : "unbound",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return { sourceSession: null, isSubagent: false, lineageStatus: "unbound" };
|
|
385
|
+
}
|
|
386
|
+
export function resolveSourceSessionFromMcpContext(metadata, options = {}) {
|
|
387
|
+
return resolveSourceSessionRequestContext(metadata, options).sourceSession;
|
|
388
|
+
}
|
|
211
389
|
export function resolveSourceSessionFromBindingToken(bindingToken, options = {}) {
|
|
212
390
|
const normalizedToken = bindingToken.trim().toLowerCase();
|
|
213
391
|
if (!UUID_RE.test(normalizedToken)) {
|
|
@@ -231,11 +409,11 @@ export function resolveSourceSessionFromBindingToken(bindingToken, options = {})
|
|
|
231
409
|
: provider === "claude-code"
|
|
232
410
|
? roots.claudeCode
|
|
233
411
|
: roots.cowork;
|
|
234
|
-
for (const file of recentFiles(providerRoots, nowMs, maxAgeMs)) {
|
|
412
|
+
for (const file of recentFiles(providerRoots, nowMs, maxAgeMs, provider === "codex")) {
|
|
235
413
|
if (!tailContainsBinding(file, normalizedToken))
|
|
236
414
|
continue;
|
|
237
415
|
const providerSessionId = provider === "codex"
|
|
238
|
-
? codexSessionId(file)
|
|
416
|
+
? codexSessionId(file, roots.codex)
|
|
239
417
|
: provider === "claude-code"
|
|
240
418
|
? claudeCodeSessionId(file)
|
|
241
419
|
: coworkSessionId(file);
|
package/dist/v1-contract.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { MCP_DESKTOP_MANAGED, MCP_VAULT_UNLOCK_INSTRUCTION, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, withMcpVersion, } from "./package-metadata.js";
|
|
3
3
|
export const canonicalToolNames = {
|
|
4
4
|
bindSourceSession: "bind_source_session",
|
|
5
|
+
linkWorkspaceTicketSession: "link_workspace_ticket_session",
|
|
5
6
|
search: "search_memories",
|
|
6
7
|
save: "save_conversation",
|
|
7
8
|
timeRange: "get_memories_by_time_range",
|
|
@@ -59,6 +60,7 @@ const READ_ONLY_TOOL_NAMES = new Set([
|
|
|
59
60
|
]);
|
|
60
61
|
const IDEMPOTENT_WRITE_TOOL_NAMES = new Set([
|
|
61
62
|
canonicalToolNames.bindSourceSession,
|
|
63
|
+
canonicalToolNames.linkWorkspaceTicketSession,
|
|
62
64
|
canonicalToolNames.recordCitations,
|
|
63
65
|
canonicalToolNames.setGroupSessionSharing,
|
|
64
66
|
canonicalToolNames.updateGroupProfile,
|
|
@@ -78,6 +80,7 @@ const REQUIRES_USER_INTERACTION_TOOL_NAMES = new Set([
|
|
|
78
80
|
]);
|
|
79
81
|
const ALWAYS_LOAD_TOOL_NAMES = new Set([
|
|
80
82
|
canonicalToolNames.bindSourceSession,
|
|
83
|
+
canonicalToolNames.linkWorkspaceTicketSession,
|
|
81
84
|
canonicalToolNames.search,
|
|
82
85
|
canonicalToolNames.save,
|
|
83
86
|
]);
|
|
@@ -226,6 +229,11 @@ export const saveConversationSchema = z.object({
|
|
|
226
229
|
export const bindSourceSessionSchema = z.object({
|
|
227
230
|
bindingToken: z.string().uuid(),
|
|
228
231
|
});
|
|
232
|
+
export const linkWorkspaceTicketSessionSchema = z.object({
|
|
233
|
+
...triggerMetadataSchema,
|
|
234
|
+
ticketId: z.string().uuid(),
|
|
235
|
+
workspaceId: z.string().uuid().optional(),
|
|
236
|
+
});
|
|
229
237
|
const dateBoundarySchema = z.string().trim().min(1).refine((value) => Number.isFinite(Date.parse(value)), "Expected an ISO-8601 date or date-time string");
|
|
230
238
|
export const timeRangeSchema = z.object({
|
|
231
239
|
...triggerMetadataSchema,
|
|
@@ -467,6 +475,32 @@ export function listToolSpecs(opts = {}) {
|
|
|
467
475
|
required: ["bindingToken"],
|
|
468
476
|
},
|
|
469
477
|
},
|
|
478
|
+
{
|
|
479
|
+
name: canonicalToolNames.linkWorkspaceTicketSession,
|
|
480
|
+
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. If the current source session is already verified, EchoMem links it immediately. Otherwise Echo Desktop can recover this exact tool invocation from the local transcript and finish the verified link; never guess or ask the user for a source-session/context ID."),
|
|
482
|
+
inputSchema: {
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
ticketId: {
|
|
486
|
+
type: "string",
|
|
487
|
+
format: "uuid",
|
|
488
|
+
description: "The Echo workspace ticket UUID from the user's message or structured launch marker.",
|
|
489
|
+
},
|
|
490
|
+
workspaceId: {
|
|
491
|
+
type: "string",
|
|
492
|
+
format: "uuid",
|
|
493
|
+
description: "Optional Echo workspace UUID when it is present in the structured launch marker.",
|
|
494
|
+
},
|
|
495
|
+
triggerMessage: {
|
|
496
|
+
type: "string",
|
|
497
|
+
description: "Optional user message that expressed the ticket-linking intent.",
|
|
498
|
+
},
|
|
499
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
500
|
+
},
|
|
501
|
+
required: ["ticketId"],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
470
504
|
{
|
|
471
505
|
name: canonicalToolNames.search,
|
|
472
506
|
title: "Search your memories by topic",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.51",
|
|
4
4
|
"description": "EchoMem MCP bridge for cross-agent memory, local history import, and recall",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"test:ui": "npm run build && node test/setup-ui.test.mjs",
|
|
32
32
|
"test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
|
|
33
33
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
34
|
-
"test": "npm run build && node test/
|
|
34
|
+
"test:config-safety": "npm run build && node test/config-safety.test.mjs",
|
|
35
|
+
"test:mcp-control": "npm run build && node test/mcp-control.test.mjs",
|
|
36
|
+
"test": "npm run build && node test/source-session.test.mjs && node test/source-session-hook.test.mjs && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/workspace-selector.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/config-safety.test.mjs && node test/mcp-control.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs && node test/durable-entry.test.mjs && node test/durable-reexec.test.mjs && node test/doctor.test.mjs && node test/environment-matrix.test.mjs",
|
|
35
37
|
"prepack": "npm run build"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|