devsh-memory-mcp 0.3.9 → 0.4.0
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 +1 -1
- package/dist/{chunk-4CUOHQV6.js → chunk-R25AQROL.js} +883 -12
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Logged items are reviewed by team leads and may be promoted to active orchestrat
|
|
|
125
125
|
|----------|-------------|
|
|
126
126
|
| `CMUX_TASK_RUN_JWT` | JWT for authenticating orchestration API calls |
|
|
127
127
|
| `CMUX_ORCHESTRATION_ID` | Current orchestration session ID |
|
|
128
|
-
| `CMUX_API_BASE_URL` | API base URL (default: https://cmux.
|
|
128
|
+
| `CMUX_API_BASE_URL` | API base URL (default: https://cmux-www.karldigi.dev) |
|
|
129
129
|
|
|
130
130
|
## Memory Directory Structure
|
|
131
131
|
|
|
@@ -23,6 +23,7 @@ function extractTeamIdFromJwt(jwt) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
var DEFAULT_MEMORY_DIR = "/root/lifecycle/memory";
|
|
26
|
+
var DEFAULT_CMUX_API_BASE_URL = "https://cmux-www.karldigi.dev";
|
|
26
27
|
function createMemoryMcpServer(config) {
|
|
27
28
|
const memoryDir = config?.memoryDir ?? DEFAULT_MEMORY_DIR;
|
|
28
29
|
const resolvedAgentName = config?.agentName ?? process.env.CMUX_AGENT_NAME;
|
|
@@ -37,9 +38,316 @@ function createMemoryMcpServer(config) {
|
|
|
37
38
|
const orchestrationDir = path.join(memoryDir, "orchestration");
|
|
38
39
|
const mailboxPath = path.join(memoryDir, "MAILBOX.json");
|
|
39
40
|
const tasksPath = path.join(memoryDir, "TASKS.json");
|
|
41
|
+
const usageStatsPath = path.join(memoryDir, "USAGE_STATS.json");
|
|
40
42
|
const planPath = path.join(orchestrationDir, "PLAN.json");
|
|
41
43
|
const agentsPath = path.join(orchestrationDir, "AGENTS.json");
|
|
42
44
|
const eventsPath = path.join(orchestrationDir, "EVENTS.jsonl");
|
|
45
|
+
function readUsageStats() {
|
|
46
|
+
const content = readFile(usageStatsPath);
|
|
47
|
+
if (!content) return { version: 1, entries: {} };
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(content);
|
|
50
|
+
} catch {
|
|
51
|
+
return { version: 1, entries: {} };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function writeUsageStats(stats) {
|
|
55
|
+
return writeFile(usageStatsPath, JSON.stringify(stats, null, 2));
|
|
56
|
+
}
|
|
57
|
+
function trackRead(memoryType) {
|
|
58
|
+
const stats = readUsageStats();
|
|
59
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
60
|
+
if (!stats.entries[memoryType]) {
|
|
61
|
+
stats.entries[memoryType] = {
|
|
62
|
+
readCount: 0,
|
|
63
|
+
lastRead: now,
|
|
64
|
+
createdAt: now
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
stats.entries[memoryType].readCount++;
|
|
68
|
+
stats.entries[memoryType].lastRead = now;
|
|
69
|
+
writeUsageStats(stats);
|
|
70
|
+
}
|
|
71
|
+
function trackWrite(memoryType) {
|
|
72
|
+
const stats = readUsageStats();
|
|
73
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
74
|
+
if (!stats.entries[memoryType]) {
|
|
75
|
+
stats.entries[memoryType] = {
|
|
76
|
+
readCount: 0,
|
|
77
|
+
lastRead: now,
|
|
78
|
+
createdAt: now
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
stats.entries[memoryType].lastWrite = now;
|
|
82
|
+
writeUsageStats(stats);
|
|
83
|
+
}
|
|
84
|
+
let syncConfig = null;
|
|
85
|
+
let syncConfigResolved = false;
|
|
86
|
+
const debounceMap = /* @__PURE__ */ new Map();
|
|
87
|
+
const retryQueue = [];
|
|
88
|
+
const DEBOUNCE_MS = 2e3;
|
|
89
|
+
const MAX_RETRIES = 3;
|
|
90
|
+
const SYNC_CONFIG_CACHE_PATH = path.join(memoryDir, ".sync-config.json");
|
|
91
|
+
const SYNC_FILE_MAP = {
|
|
92
|
+
append_daily_log: {
|
|
93
|
+
memoryType: "daily",
|
|
94
|
+
getFilePath: () => path.join(dailyDir, `${getTodayDateString()}.md`),
|
|
95
|
+
getFileName: (date) => `daily/${date ?? getTodayDateString()}.md`,
|
|
96
|
+
getDate: () => getTodayDateString()
|
|
97
|
+
},
|
|
98
|
+
update_knowledge: {
|
|
99
|
+
memoryType: "knowledge",
|
|
100
|
+
getFilePath: () => path.join(knowledgeDir, "MEMORY.md"),
|
|
101
|
+
getFileName: () => "knowledge/MEMORY.md"
|
|
102
|
+
},
|
|
103
|
+
add_task: {
|
|
104
|
+
memoryType: "tasks",
|
|
105
|
+
getFilePath: () => tasksPath,
|
|
106
|
+
getFileName: () => "TASKS.json"
|
|
107
|
+
},
|
|
108
|
+
update_task: {
|
|
109
|
+
memoryType: "tasks",
|
|
110
|
+
getFilePath: () => tasksPath,
|
|
111
|
+
getFileName: () => "TASKS.json"
|
|
112
|
+
},
|
|
113
|
+
send_message: {
|
|
114
|
+
memoryType: "mailbox",
|
|
115
|
+
getFilePath: () => mailboxPath,
|
|
116
|
+
getFileName: () => "MAILBOX.json"
|
|
117
|
+
},
|
|
118
|
+
mark_read: {
|
|
119
|
+
memoryType: "mailbox",
|
|
120
|
+
getFilePath: () => mailboxPath,
|
|
121
|
+
getFileName: () => "MAILBOX.json"
|
|
122
|
+
},
|
|
123
|
+
append_event: {
|
|
124
|
+
memoryType: "events",
|
|
125
|
+
getFilePath: () => eventsPath,
|
|
126
|
+
getFileName: () => "orchestration/EVENTS.jsonl"
|
|
127
|
+
},
|
|
128
|
+
update_plan_task: {
|
|
129
|
+
memoryType: "events",
|
|
130
|
+
getFilePath: () => eventsPath,
|
|
131
|
+
getFileName: () => "orchestration/EVENTS.jsonl"
|
|
132
|
+
},
|
|
133
|
+
check_stale_entries: {
|
|
134
|
+
memoryType: "knowledge",
|
|
135
|
+
getFilePath: () => path.join(knowledgeDir, "MEMORY.md"),
|
|
136
|
+
getFileName: () => "knowledge/MEMORY.md"
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
function readDotEnv(envPath) {
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
142
|
+
const vars = {};
|
|
143
|
+
for (const line of content.split("\n")) {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
146
|
+
const eqIndex = trimmed.indexOf("=");
|
|
147
|
+
if (eqIndex === -1) continue;
|
|
148
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
149
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
150
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
151
|
+
value = value.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
vars[key] = value;
|
|
154
|
+
}
|
|
155
|
+
return vars;
|
|
156
|
+
} catch {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function resolveSyncConfig() {
|
|
161
|
+
const envJwt = process.env.CMUX_TASK_RUN_JWT;
|
|
162
|
+
const envCallbackUrl = process.env.CMUX_CALLBACK_URL;
|
|
163
|
+
if (envJwt && envCallbackUrl) {
|
|
164
|
+
console.error(`[devsh-memory-mcp] Sync enabled via env vars: ${envCallbackUrl}`);
|
|
165
|
+
return { syncUrl: envCallbackUrl, authToken: envJwt };
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const cachedRaw = fs.readFileSync(SYNC_CONFIG_CACHE_PATH, "utf-8");
|
|
169
|
+
const cached = JSON.parse(cachedRaw);
|
|
170
|
+
if (cached.jwt && cached.syncUrl && cached.expiresAt > Date.now()) {
|
|
171
|
+
console.error(`[devsh-memory-mcp] Sync enabled via cached config: ${cached.syncUrl}`);
|
|
172
|
+
return { syncUrl: cached.syncUrl, authToken: cached.jwt };
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
const envPaths = [
|
|
177
|
+
path.join(memoryDir, "..", "..", ".env"),
|
|
178
|
+
"/root/workspace/.env",
|
|
179
|
+
path.join(process.env.HOME ?? "/root", "workspace", ".env")
|
|
180
|
+
];
|
|
181
|
+
let jwtSecret;
|
|
182
|
+
let convexUrl;
|
|
183
|
+
let teamId;
|
|
184
|
+
for (const envPath of envPaths) {
|
|
185
|
+
const vars = readDotEnv(envPath);
|
|
186
|
+
jwtSecret = jwtSecret ?? vars.CMUX_TASK_RUN_JWT_SECRET;
|
|
187
|
+
convexUrl = convexUrl ?? vars.CONVEX_SITE_URL ?? vars.CONVEX_SELF_HOSTED_URL;
|
|
188
|
+
teamId = teamId ?? vars.CMUX_TEAM_ID;
|
|
189
|
+
}
|
|
190
|
+
teamId = teamId ?? process.env.CMUX_TEAM_ID;
|
|
191
|
+
if (!jwtSecret || !convexUrl) {
|
|
192
|
+
console.error("[devsh-memory-mcp] Sync disabled: no JWT or Convex URL found");
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (!teamId) {
|
|
196
|
+
console.error("[devsh-memory-mcp] Sync disabled: no teamId found for bootstrap");
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(`${convexUrl}/api/memory/bootstrap`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
"x-cmux-bootstrap-key": jwtSecret
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
teamId,
|
|
208
|
+
agentName
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const errText = await response.text().catch(() => "");
|
|
213
|
+
console.error(`[devsh-memory-mcp] Bootstrap failed: ${response.status} ${errText}`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const result = await response.json();
|
|
217
|
+
if (!result.ok || !result.jwt) {
|
|
218
|
+
console.error("[devsh-memory-mcp] Bootstrap returned invalid response");
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const cacheData = {
|
|
222
|
+
jwt: result.jwt,
|
|
223
|
+
taskRunId: result.taskRunId,
|
|
224
|
+
syncUrl: convexUrl,
|
|
225
|
+
expiresAt: Date.now() + 6 * 24 * 60 * 60 * 1e3
|
|
226
|
+
};
|
|
227
|
+
try {
|
|
228
|
+
fs.writeFileSync(SYNC_CONFIG_CACHE_PATH, JSON.stringify(cacheData), "utf-8");
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
console.error(
|
|
232
|
+
`[devsh-memory-mcp] Sync enabled via bootstrap: ${convexUrl} (reused=${result.reused})`
|
|
233
|
+
);
|
|
234
|
+
return { syncUrl: convexUrl, authToken: result.jwt };
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("[devsh-memory-mcp] Bootstrap error:", error);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function doSync(memoryType, content, fileName, date) {
|
|
241
|
+
if (!syncConfig) return;
|
|
242
|
+
const url = `${syncConfig.syncUrl}/api/memory/sync`;
|
|
243
|
+
const body = {
|
|
244
|
+
files: [
|
|
245
|
+
{
|
|
246
|
+
memoryType,
|
|
247
|
+
content,
|
|
248
|
+
fileName,
|
|
249
|
+
date
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
"x-cmux-token": syncConfig.authToken
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify(body)
|
|
261
|
+
});
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
const errText = await response.text().catch(() => "");
|
|
264
|
+
console.error(
|
|
265
|
+
`[devsh-memory-mcp] Sync failed for ${memoryType}: ${response.status} ${errText}`
|
|
266
|
+
);
|
|
267
|
+
enqueueRetry(memoryType, content, fileName, date);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
drainRetryQueue();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error(
|
|
273
|
+
`[devsh-memory-mcp] Sync error for ${memoryType}:`,
|
|
274
|
+
error instanceof Error ? error.message : error
|
|
275
|
+
);
|
|
276
|
+
enqueueRetry(memoryType, content, fileName, date);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function enqueueRetry(memoryType, content, fileName, date) {
|
|
280
|
+
const key = date ? `${memoryType}:${date}` : memoryType;
|
|
281
|
+
const existingIdx = retryQueue.findIndex(
|
|
282
|
+
(r) => (r.date ? `${r.memoryType}:${r.date}` : r.memoryType) === key
|
|
283
|
+
);
|
|
284
|
+
if (existingIdx >= 0) {
|
|
285
|
+
const existing = retryQueue[existingIdx];
|
|
286
|
+
if (existing.retryCount >= MAX_RETRIES) {
|
|
287
|
+
console.error(
|
|
288
|
+
`[devsh-memory-mcp] Dropping retry for ${memoryType} after ${MAX_RETRIES} attempts`
|
|
289
|
+
);
|
|
290
|
+
retryQueue.splice(existingIdx, 1);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
retryQueue[existingIdx] = {
|
|
294
|
+
memoryType,
|
|
295
|
+
content,
|
|
296
|
+
fileName,
|
|
297
|
+
date,
|
|
298
|
+
retryCount: existing.retryCount + 1
|
|
299
|
+
};
|
|
300
|
+
} else {
|
|
301
|
+
retryQueue.push({ memoryType, content, fileName, date, retryCount: 1 });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function drainRetryQueue() {
|
|
305
|
+
if (!syncConfig || retryQueue.length === 0) return;
|
|
306
|
+
const items = [...retryQueue];
|
|
307
|
+
retryQueue.length = 0;
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
if (item.retryCount >= MAX_RETRIES) {
|
|
310
|
+
console.error(
|
|
311
|
+
`[devsh-memory-mcp] Dropping retry for ${item.memoryType} after ${MAX_RETRIES} attempts`
|
|
312
|
+
);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
await doSync(item.memoryType, item.content, item.fileName, item.date);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function scheduleSyncForFile(toolName) {
|
|
319
|
+
if (!syncConfigResolved) return;
|
|
320
|
+
if (!syncConfig) return;
|
|
321
|
+
const mapping = SYNC_FILE_MAP[toolName];
|
|
322
|
+
if (!mapping) return;
|
|
323
|
+
const { memoryType } = mapping;
|
|
324
|
+
const date = mapping.getDate?.();
|
|
325
|
+
const debounceKey = date ? `${memoryType}:${date}` : memoryType;
|
|
326
|
+
let entry = debounceMap.get(debounceKey);
|
|
327
|
+
if (!entry) {
|
|
328
|
+
entry = { timer: null };
|
|
329
|
+
debounceMap.set(debounceKey, entry);
|
|
330
|
+
}
|
|
331
|
+
if (entry.timer) {
|
|
332
|
+
clearTimeout(entry.timer);
|
|
333
|
+
}
|
|
334
|
+
entry.timer = setTimeout(() => {
|
|
335
|
+
const filePath = mapping.getFilePath();
|
|
336
|
+
const content = readFile(filePath);
|
|
337
|
+
if (!content) return;
|
|
338
|
+
const fileName = mapping.getFileName(date);
|
|
339
|
+
doSync(memoryType, content, fileName, date).catch((err) => {
|
|
340
|
+
console.error("[devsh-memory-mcp] Sync error in debounce callback:", err);
|
|
341
|
+
});
|
|
342
|
+
}, DEBOUNCE_MS);
|
|
343
|
+
}
|
|
344
|
+
resolveSyncConfig().then((config2) => {
|
|
345
|
+
syncConfig = config2;
|
|
346
|
+
syncConfigResolved = true;
|
|
347
|
+
}).catch((err) => {
|
|
348
|
+
console.error("[devsh-memory-mcp] Sync config resolution failed:", err);
|
|
349
|
+
syncConfigResolved = true;
|
|
350
|
+
});
|
|
43
351
|
function readFile(filePath) {
|
|
44
352
|
try {
|
|
45
353
|
return fs.readFileSync(filePath, "utf-8");
|
|
@@ -79,6 +387,9 @@ function createMemoryMcpServer(config) {
|
|
|
79
387
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
80
388
|
}
|
|
81
389
|
}
|
|
390
|
+
function getCmuxApiBaseUrl() {
|
|
391
|
+
return process.env.CMUX_API_BASE_URL ?? DEFAULT_CMUX_API_BASE_URL;
|
|
392
|
+
}
|
|
82
393
|
function readPlan() {
|
|
83
394
|
const content = readFile(planPath);
|
|
84
395
|
if (!content) return null;
|
|
@@ -241,6 +552,31 @@ function createMemoryMcpServer(config) {
|
|
|
241
552
|
required: ["query"]
|
|
242
553
|
}
|
|
243
554
|
},
|
|
555
|
+
{
|
|
556
|
+
name: "get_memory_usage",
|
|
557
|
+
description: "Get usage statistics for memory files. Shows read/write counts and timestamps for freshness tracking.",
|
|
558
|
+
inputSchema: {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "archive_stale_memories",
|
|
565
|
+
description: "Archive stale memory entries from MEMORY.md. Moves low-freshness entries to an archive section. Use dryRun=true to preview without changes.",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: "object",
|
|
568
|
+
properties: {
|
|
569
|
+
dryRun: {
|
|
570
|
+
type: "boolean",
|
|
571
|
+
description: "Preview changes without modifying files (default: true)"
|
|
572
|
+
},
|
|
573
|
+
maxScoreToArchive: {
|
|
574
|
+
type: "number",
|
|
575
|
+
description: "Archive entries with freshness score below this threshold (default: 30)"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
},
|
|
244
580
|
{
|
|
245
581
|
name: "send_message",
|
|
246
582
|
description: 'Send a message to another agent on the same task. Use "*" to broadcast to all agents. Aligned with Claude Code SendMessage pattern.',
|
|
@@ -603,6 +939,14 @@ function createMemoryMcpServer(config) {
|
|
|
603
939
|
properties: {}
|
|
604
940
|
}
|
|
605
941
|
},
|
|
942
|
+
{
|
|
943
|
+
name: "get_context_health",
|
|
944
|
+
description: "Get context health summary for the current task run. Returns context window usage, warning state, and recent compaction events. Use this to monitor context pressure and decide when to summarize or archive context.",
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: "object",
|
|
947
|
+
properties: {}
|
|
948
|
+
}
|
|
949
|
+
},
|
|
606
950
|
{
|
|
607
951
|
name: "bind_provider_session",
|
|
608
952
|
description: "Bind a provider-specific session ID to the current task. Use to enable session resume on task retry. Claude agents should call this with their session ID, Codex agents with their thread ID.",
|
|
@@ -750,6 +1094,56 @@ function createMemoryMcpServer(config) {
|
|
|
750
1094
|
}
|
|
751
1095
|
}
|
|
752
1096
|
}
|
|
1097
|
+
},
|
|
1098
|
+
// Operator Input Queue Tools (Active-Turn Steering)
|
|
1099
|
+
{
|
|
1100
|
+
name: "get_pending_operator_inputs",
|
|
1101
|
+
description: "Check if there are pending operator steering inputs in the queue. Call this at turn boundaries to see if the operator has sent guidance. Returns queue status without draining.",
|
|
1102
|
+
inputSchema: {
|
|
1103
|
+
type: "object",
|
|
1104
|
+
properties: {
|
|
1105
|
+
orchestrationId: {
|
|
1106
|
+
type: "string",
|
|
1107
|
+
description: "The orchestration ID (optional, uses CMUX_ORCHESTRATION_ID env var if not provided)"
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: "acknowledge_operator_inputs",
|
|
1114
|
+
description: "Drain and acknowledge all pending operator inputs. Returns merged content (newline-separated, priority-ordered). Call this at turn boundaries to process operator steering.",
|
|
1115
|
+
inputSchema: {
|
|
1116
|
+
type: "object",
|
|
1117
|
+
properties: {
|
|
1118
|
+
orchestrationId: {
|
|
1119
|
+
type: "string",
|
|
1120
|
+
description: "The orchestration ID (optional, uses CMUX_ORCHESTRATION_ID env var if not provided)"
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
name: "queue_operator_input",
|
|
1127
|
+
description: "Queue an operator steering input for the agent. Use this as a head agent to send guidance to workers, or for testing. Returns QUEUE_FULL error if at capacity.",
|
|
1128
|
+
inputSchema: {
|
|
1129
|
+
type: "object",
|
|
1130
|
+
properties: {
|
|
1131
|
+
orchestrationId: {
|
|
1132
|
+
type: "string",
|
|
1133
|
+
description: "The orchestration ID (optional, uses CMUX_ORCHESTRATION_ID env var if not provided)"
|
|
1134
|
+
},
|
|
1135
|
+
content: {
|
|
1136
|
+
type: "string",
|
|
1137
|
+
description: "The steering instruction content"
|
|
1138
|
+
},
|
|
1139
|
+
priority: {
|
|
1140
|
+
type: "string",
|
|
1141
|
+
enum: ["high", "normal", "low"],
|
|
1142
|
+
description: "Input priority (default: normal). High for interrupts, low for background guidance."
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
required: ["content"]
|
|
1146
|
+
}
|
|
753
1147
|
}
|
|
754
1148
|
]
|
|
755
1149
|
}));
|
|
@@ -758,6 +1152,7 @@ function createMemoryMcpServer(config) {
|
|
|
758
1152
|
switch (name) {
|
|
759
1153
|
case "read_memory": {
|
|
760
1154
|
const { type, includeCompleted, limit } = args;
|
|
1155
|
+
trackRead(type);
|
|
761
1156
|
let content = null;
|
|
762
1157
|
if (type === "knowledge") {
|
|
763
1158
|
content = readFile(path.join(knowledgeDir, "MEMORY.md"));
|
|
@@ -814,6 +1209,120 @@ function createMemoryMcpServer(config) {
|
|
|
814
1209
|
const formatted = results.map((r) => `[${r.source}${r.line ? `:${r.line}` : ""}] ${r.content}`).join("\n");
|
|
815
1210
|
return { content: [{ type: "text", text: formatted }] };
|
|
816
1211
|
}
|
|
1212
|
+
case "get_memory_usage": {
|
|
1213
|
+
const stats = readUsageStats();
|
|
1214
|
+
if (Object.keys(stats.entries).length === 0) {
|
|
1215
|
+
return { content: [{ type: "text", text: "No usage statistics recorded yet." }] };
|
|
1216
|
+
}
|
|
1217
|
+
const now = Date.now();
|
|
1218
|
+
const entries = Object.entries(stats.entries).map(([key, value]) => {
|
|
1219
|
+
const daysSinceRead = Math.floor((now - new Date(value.lastRead).getTime()) / (1e3 * 60 * 60 * 24));
|
|
1220
|
+
const daysSinceWrite = value.lastWrite ? Math.floor((now - new Date(value.lastWrite).getTime()) / (1e3 * 60 * 60 * 24)) : null;
|
|
1221
|
+
const recencyScore = Math.max(0, 100 - daysSinceRead * 3);
|
|
1222
|
+
const usageScore = Math.min(100, value.readCount * 10);
|
|
1223
|
+
const priorityScore = key.includes("P0") ? 100 : key.includes("P1") ? 50 : key.includes("P2") ? 25 : 50;
|
|
1224
|
+
const freshnessScore = Math.round(recencyScore * 0.4 + usageScore * 0.4 + priorityScore * 0.2);
|
|
1225
|
+
let recommendation;
|
|
1226
|
+
if (freshnessScore >= 60) recommendation = "keep";
|
|
1227
|
+
else if (freshnessScore >= 30) recommendation = "review";
|
|
1228
|
+
else recommendation = "archive";
|
|
1229
|
+
return {
|
|
1230
|
+
type: key,
|
|
1231
|
+
readCount: value.readCount,
|
|
1232
|
+
daysSinceRead,
|
|
1233
|
+
daysSinceWrite,
|
|
1234
|
+
freshnessScore,
|
|
1235
|
+
recommendation
|
|
1236
|
+
};
|
|
1237
|
+
});
|
|
1238
|
+
const keepCount = entries.filter((e) => e.recommendation === "keep").length;
|
|
1239
|
+
const reviewCount = entries.filter((e) => e.recommendation === "review").length;
|
|
1240
|
+
const archiveCount = entries.filter((e) => e.recommendation === "archive").length;
|
|
1241
|
+
return {
|
|
1242
|
+
content: [{
|
|
1243
|
+
type: "text",
|
|
1244
|
+
text: JSON.stringify({
|
|
1245
|
+
summary: { keep: keepCount, review: reviewCount, archive: archiveCount },
|
|
1246
|
+
entries
|
|
1247
|
+
}, null, 2)
|
|
1248
|
+
}]
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
case "archive_stale_memories": {
|
|
1252
|
+
const { dryRun = true, maxScoreToArchive = 30 } = args;
|
|
1253
|
+
const knowledgePath = path.join(knowledgeDir, "MEMORY.md");
|
|
1254
|
+
const content = readFile(knowledgePath);
|
|
1255
|
+
if (!content) {
|
|
1256
|
+
return { content: [{ type: "text", text: "No MEMORY.md file found." }] };
|
|
1257
|
+
}
|
|
1258
|
+
const lines = content.split("\n");
|
|
1259
|
+
const dateEntryPattern = /^- \[(\d{4}-\d{2}-\d{2})\] (.+)$/;
|
|
1260
|
+
const now = Date.now();
|
|
1261
|
+
const entriesToArchive = [];
|
|
1262
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1263
|
+
const match = lines[i].match(dateEntryPattern);
|
|
1264
|
+
if (match) {
|
|
1265
|
+
const entryDate = new Date(match[1]).getTime();
|
|
1266
|
+
const daysSince = Math.floor((now - entryDate) / (1e3 * 60 * 60 * 24));
|
|
1267
|
+
const score = Math.max(0, 100 - daysSince * 2);
|
|
1268
|
+
if (score < maxScoreToArchive) {
|
|
1269
|
+
entriesToArchive.push({
|
|
1270
|
+
line: i,
|
|
1271
|
+
date: match[1],
|
|
1272
|
+
text: match[2],
|
|
1273
|
+
score
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (entriesToArchive.length === 0) {
|
|
1279
|
+
return { content: [{ type: "text", text: "No stale entries found to archive." }] };
|
|
1280
|
+
}
|
|
1281
|
+
if (dryRun) {
|
|
1282
|
+
return {
|
|
1283
|
+
content: [{
|
|
1284
|
+
type: "text",
|
|
1285
|
+
text: JSON.stringify({
|
|
1286
|
+
dryRun: true,
|
|
1287
|
+
wouldArchive: entriesToArchive.length,
|
|
1288
|
+
entries: entriesToArchive,
|
|
1289
|
+
message: "Set dryRun=false to actually archive these entries."
|
|
1290
|
+
}, null, 2)
|
|
1291
|
+
}]
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const linesToRemove = new Set(entriesToArchive.map((e) => e.line));
|
|
1295
|
+
const newLines = lines.filter((_, i) => !linesToRemove.has(i));
|
|
1296
|
+
const archiveHeader = "## Archive (Stale Entries)";
|
|
1297
|
+
let archiveContent = newLines.join("\n");
|
|
1298
|
+
if (!archiveContent.includes(archiveHeader)) {
|
|
1299
|
+
archiveContent += `
|
|
1300
|
+
|
|
1301
|
+
${archiveHeader}
|
|
1302
|
+
<!-- Entries moved here by archive_stale_memories tool -->
|
|
1303
|
+
`;
|
|
1304
|
+
}
|
|
1305
|
+
const archiveEntries = entriesToArchive.map((e) => `- [${e.date}] [archived] ${e.text}`).join("\n");
|
|
1306
|
+
archiveContent = archiveContent.replace(
|
|
1307
|
+
archiveHeader,
|
|
1308
|
+
`${archiveHeader}
|
|
1309
|
+
${archiveEntries}`
|
|
1310
|
+
);
|
|
1311
|
+
if (writeFile(knowledgePath, archiveContent)) {
|
|
1312
|
+
trackWrite("knowledge.archive");
|
|
1313
|
+
scheduleSyncForFile("update_knowledge");
|
|
1314
|
+
return {
|
|
1315
|
+
content: [{
|
|
1316
|
+
type: "text",
|
|
1317
|
+
text: JSON.stringify({
|
|
1318
|
+
archived: entriesToArchive.length,
|
|
1319
|
+
entries: entriesToArchive.map((e) => e.text.slice(0, 50) + "...")
|
|
1320
|
+
}, null, 2)
|
|
1321
|
+
}]
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return { content: [{ type: "text", text: "Failed to write archived content." }] };
|
|
1325
|
+
}
|
|
817
1326
|
case "send_message": {
|
|
818
1327
|
const { to, message, type, correlationId, replyTo, priority, metadata } = args;
|
|
819
1328
|
const mailbox = readMailbox();
|
|
@@ -832,6 +1341,7 @@ function createMemoryMcpServer(config) {
|
|
|
832
1341
|
};
|
|
833
1342
|
mailbox.messages.push(newMessage);
|
|
834
1343
|
writeMailbox(mailbox);
|
|
1344
|
+
scheduleSyncForFile("send_message");
|
|
835
1345
|
const responseText = correlationId ? `Message sent successfully. ID: ${newMessage.id}, correlationId: ${correlationId}` : `Message sent successfully. ID: ${newMessage.id}`;
|
|
836
1346
|
return { content: [{ type: "text", text: responseText }] };
|
|
837
1347
|
}
|
|
@@ -876,6 +1386,7 @@ function createMemoryMcpServer(config) {
|
|
|
876
1386
|
}
|
|
877
1387
|
message.read = true;
|
|
878
1388
|
writeMailbox(mailbox);
|
|
1389
|
+
scheduleSyncForFile("mark_read");
|
|
879
1390
|
return { content: [{ type: "text", text: `Message ${messageId} marked as read.` }] };
|
|
880
1391
|
}
|
|
881
1392
|
// Write tool handlers
|
|
@@ -894,12 +1405,25 @@ function createMemoryMcpServer(config) {
|
|
|
894
1405
|
const newContent = existing + `
|
|
895
1406
|
- [${timestamp}] ${content}`;
|
|
896
1407
|
if (writeFile(logPath, newContent)) {
|
|
1408
|
+
scheduleSyncForFile("append_daily_log");
|
|
897
1409
|
return { content: [{ type: "text", text: `Appended to daily/${today}.md` }] };
|
|
898
1410
|
}
|
|
899
1411
|
return { content: [{ type: "text", text: `Failed to append to daily log` }] };
|
|
900
1412
|
}
|
|
901
1413
|
case "update_knowledge": {
|
|
902
1414
|
const { section, content } = args;
|
|
1415
|
+
if (content.length > 200) {
|
|
1416
|
+
return {
|
|
1417
|
+
content: [{
|
|
1418
|
+
type: "text",
|
|
1419
|
+
text: `Entry too long (${content.length} chars). Keep memory entries concise (max 200 chars). Consider splitting into multiple entries or being more specific.`
|
|
1420
|
+
}]
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
if (!content.trim()) {
|
|
1424
|
+
return { content: [{ type: "text", text: "Cannot add empty memory entry." }] };
|
|
1425
|
+
}
|
|
1426
|
+
trackWrite(`knowledge.${section}`);
|
|
903
1427
|
ensureDir(knowledgeDir);
|
|
904
1428
|
const knowledgePath = path.join(knowledgeDir, "MEMORY.md");
|
|
905
1429
|
let existing = readFile(knowledgePath);
|
|
@@ -934,6 +1458,45 @@ function createMemoryMcpServer(config) {
|
|
|
934
1458
|
if (headerIndex === -1) {
|
|
935
1459
|
return { content: [{ type: "text", text: `Section ${section} not found in MEMORY.md` }] };
|
|
936
1460
|
}
|
|
1461
|
+
const contentLower = content.toLowerCase();
|
|
1462
|
+
const existingEntries = existing.match(/- \[\d{4}-\d{2}-\d{2}\] .+/g) || [];
|
|
1463
|
+
for (const entry of existingEntries) {
|
|
1464
|
+
const entryText = entry.replace(/- \[\d{4}-\d{2}-\d{2}\] /, "").toLowerCase();
|
|
1465
|
+
if (entryText === contentLower || entryText.includes(contentLower) || contentLower.includes(entryText)) {
|
|
1466
|
+
return {
|
|
1467
|
+
content: [{
|
|
1468
|
+
type: "text",
|
|
1469
|
+
text: `Duplicate detected. Similar entry already exists: "${entry.slice(0, 80)}...". Update the existing entry instead of adding a duplicate.`
|
|
1470
|
+
}]
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (section === "P0") {
|
|
1475
|
+
const p0Section = existing.slice(headerIndex, existing.indexOf("## P1"));
|
|
1476
|
+
const usesPattern = /uses?\s+(\w+)/gi;
|
|
1477
|
+
const newUses = [...contentLower.matchAll(usesPattern)].map((m) => m[1]);
|
|
1478
|
+
const existingUses = [...p0Section.toLowerCase().matchAll(usesPattern)].map((m) => m[1]);
|
|
1479
|
+
const alternatives = {
|
|
1480
|
+
bun: ["npm", "yarn", "pnpm"],
|
|
1481
|
+
npm: ["bun", "yarn", "pnpm"],
|
|
1482
|
+
yarn: ["npm", "bun", "pnpm"],
|
|
1483
|
+
vitest: ["jest", "mocha"],
|
|
1484
|
+
jest: ["vitest", "mocha"]
|
|
1485
|
+
};
|
|
1486
|
+
for (const newTool of newUses) {
|
|
1487
|
+
const conflicts = alternatives[newTool] || [];
|
|
1488
|
+
for (const existingTool of existingUses) {
|
|
1489
|
+
if (conflicts.includes(existingTool)) {
|
|
1490
|
+
return {
|
|
1491
|
+
content: [{
|
|
1492
|
+
type: "text",
|
|
1493
|
+
text: `Potential P0 contradiction: New entry mentions "${newTool}" but P0 already contains "${existingTool}". These are typically alternatives. Verify this is intentional before adding.`
|
|
1494
|
+
}]
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
937
1500
|
const afterHeader = existing.slice(headerIndex + header.length);
|
|
938
1501
|
const nextSectionMatch = afterHeader.match(/\n## /);
|
|
939
1502
|
const insertPoint = nextSectionMatch ? headerIndex + header.length + (nextSectionMatch.index ?? afterHeader.length) : existing.length;
|
|
@@ -942,6 +1505,7 @@ function createMemoryMcpServer(config) {
|
|
|
942
1505
|
const actualInsertPoint = Math.min(commentEnd, insertPoint);
|
|
943
1506
|
const updated = existing.slice(0, actualInsertPoint) + newEntry + "\n" + existing.slice(actualInsertPoint);
|
|
944
1507
|
if (writeFile(knowledgePath, updated)) {
|
|
1508
|
+
scheduleSyncForFile("update_knowledge");
|
|
945
1509
|
return { content: [{ type: "text", text: `Added entry to ${section} section in MEMORY.md` }] };
|
|
946
1510
|
}
|
|
947
1511
|
return { content: [{ type: "text", text: `Failed to update MEMORY.md` }] };
|
|
@@ -960,6 +1524,7 @@ function createMemoryMcpServer(config) {
|
|
|
960
1524
|
};
|
|
961
1525
|
tasks.tasks.push(newTask);
|
|
962
1526
|
if (writeTasks(tasks)) {
|
|
1527
|
+
scheduleSyncForFile("add_task");
|
|
963
1528
|
return { content: [{ type: "text", text: `Task created with ID: ${newTask.id}` }] };
|
|
964
1529
|
}
|
|
965
1530
|
return { content: [{ type: "text", text: `Failed to create task` }] };
|
|
@@ -974,6 +1539,7 @@ function createMemoryMcpServer(config) {
|
|
|
974
1539
|
task.status = status;
|
|
975
1540
|
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
976
1541
|
if (writeTasks(tasks)) {
|
|
1542
|
+
scheduleSyncForFile("update_task");
|
|
977
1543
|
return { content: [{ type: "text", text: `Task ${taskId} updated to status: ${status}` }] };
|
|
978
1544
|
}
|
|
979
1545
|
return { content: [{ type: "text", text: `Failed to update task` }] };
|
|
@@ -1003,6 +1569,7 @@ function createMemoryMcpServer(config) {
|
|
|
1003
1569
|
if (agentName2) eventObj.agentName = agentName2;
|
|
1004
1570
|
if (taskRunId) eventObj.taskRunId = taskRunId;
|
|
1005
1571
|
if (appendEvent(eventObj)) {
|
|
1572
|
+
scheduleSyncForFile("append_event");
|
|
1006
1573
|
return { content: [{ type: "text", text: `Event appended to EVENTS.jsonl` }] };
|
|
1007
1574
|
}
|
|
1008
1575
|
return { content: [{ type: "text", text: `Failed to append event` }] };
|
|
@@ -1027,6 +1594,7 @@ function createMemoryMcpServer(config) {
|
|
|
1027
1594
|
task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1028
1595
|
}
|
|
1029
1596
|
if (writePlan(plan)) {
|
|
1597
|
+
scheduleSyncForFile("update_plan_task");
|
|
1030
1598
|
return { content: [{ type: "text", text: `Plan task ${taskId} updated to status: ${status}` }] };
|
|
1031
1599
|
}
|
|
1032
1600
|
return { content: [{ type: "text", text: `Failed to update plan task` }] };
|
|
@@ -1035,7 +1603,7 @@ function createMemoryMcpServer(config) {
|
|
|
1035
1603
|
const { orchestrationId: argOrchId } = args;
|
|
1036
1604
|
const orchestrationId = argOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
|
|
1037
1605
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1038
|
-
const apiBaseUrl =
|
|
1606
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1039
1607
|
if (!orchestrationId) {
|
|
1040
1608
|
return {
|
|
1041
1609
|
content: [{
|
|
@@ -1146,7 +1714,7 @@ function createMemoryMcpServer(config) {
|
|
|
1146
1714
|
const { orchestrationId: argOrchId, headAgentStatus, message, taskIds } = args;
|
|
1147
1715
|
const orchestrationId = argOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
|
|
1148
1716
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1149
|
-
const apiBaseUrl =
|
|
1717
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1150
1718
|
if (!orchestrationId) {
|
|
1151
1719
|
return {
|
|
1152
1720
|
content: [{
|
|
@@ -1483,7 +2051,7 @@ function createMemoryMcpServer(config) {
|
|
|
1483
2051
|
case "list_spawned_agents": {
|
|
1484
2052
|
const { status: filterStatus } = args;
|
|
1485
2053
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1486
|
-
const apiBaseUrl =
|
|
2054
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1487
2055
|
const orchestrationId = process.env.CMUX_ORCHESTRATION_ID;
|
|
1488
2056
|
if (!jwt) {
|
|
1489
2057
|
return {
|
|
@@ -1542,7 +2110,7 @@ function createMemoryMcpServer(config) {
|
|
|
1542
2110
|
case "cancel_agent": {
|
|
1543
2111
|
const { orchestrationTaskId, cascade = false } = args;
|
|
1544
2112
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1545
|
-
const apiBaseUrl =
|
|
2113
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1546
2114
|
if (!jwt) {
|
|
1547
2115
|
return {
|
|
1548
2116
|
content: [{
|
|
@@ -1610,7 +2178,7 @@ function createMemoryMcpServer(config) {
|
|
|
1610
2178
|
}
|
|
1611
2179
|
case "get_orchestration_summary": {
|
|
1612
2180
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1613
|
-
const apiBaseUrl =
|
|
2181
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1614
2182
|
const orchestrationId = process.env.CMUX_ORCHESTRATION_ID;
|
|
1615
2183
|
if (!jwt) {
|
|
1616
2184
|
return {
|
|
@@ -1680,10 +2248,77 @@ function createMemoryMcpServer(config) {
|
|
|
1680
2248
|
};
|
|
1681
2249
|
}
|
|
1682
2250
|
}
|
|
2251
|
+
case "get_context_health": {
|
|
2252
|
+
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
2253
|
+
const callbackUrl = process.env.CMUX_CALLBACK_URL;
|
|
2254
|
+
if (!jwt) {
|
|
2255
|
+
return {
|
|
2256
|
+
content: [{
|
|
2257
|
+
type: "text",
|
|
2258
|
+
text: "CMUX_TASK_RUN_JWT environment variable not set. This tool requires JWT authentication."
|
|
2259
|
+
}]
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
if (!callbackUrl) {
|
|
2263
|
+
return {
|
|
2264
|
+
content: [{
|
|
2265
|
+
type: "text",
|
|
2266
|
+
text: "CMUX_CALLBACK_URL environment variable not set."
|
|
2267
|
+
}]
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
const url = `${callbackUrl}/api/v1/cmux/orchestration/context-health`;
|
|
2272
|
+
const response = await fetch(url, {
|
|
2273
|
+
method: "GET",
|
|
2274
|
+
headers: {
|
|
2275
|
+
"x-cmux-token": jwt
|
|
2276
|
+
}
|
|
2277
|
+
});
|
|
2278
|
+
if (!response.ok) {
|
|
2279
|
+
const errorData = await response.json().catch(() => ({}));
|
|
2280
|
+
return {
|
|
2281
|
+
content: [{
|
|
2282
|
+
type: "text",
|
|
2283
|
+
text: `Error getting context health: ${response.status} ${errorData.message ?? response.statusText}`
|
|
2284
|
+
}]
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
const data = await response.json();
|
|
2288
|
+
const usageStr = data.contextWindow ? `${data.totalInputTokens.toLocaleString()} / ${data.contextWindow.toLocaleString()} tokens (${data.usagePercent ?? 0}%)` : `${data.totalInputTokens.toLocaleString()} input tokens`;
|
|
2289
|
+
const warningStr = data.latestWarningSeverity ? `${data.latestWarningSeverity.toUpperCase()} - ${data.topWarningReasons[0] ?? "No details"}` : "None";
|
|
2290
|
+
const summary = [
|
|
2291
|
+
`Provider: ${data.provider}`,
|
|
2292
|
+
`Context Usage: ${usageStr}`,
|
|
2293
|
+
`Output Tokens: ${data.totalOutputTokens.toLocaleString()}`,
|
|
2294
|
+
`Warning State: ${warningStr}`,
|
|
2295
|
+
`Warning Count: ${data.warningCount}`,
|
|
2296
|
+
`Compaction Count: ${data.recentCompactionCount}`
|
|
2297
|
+
].join("\n");
|
|
2298
|
+
return {
|
|
2299
|
+
content: [{
|
|
2300
|
+
type: "text",
|
|
2301
|
+
text: `Context Health Summary:
|
|
2302
|
+
${summary}
|
|
2303
|
+
|
|
2304
|
+
Raw Data:
|
|
2305
|
+
${JSON.stringify(data, null, 2)}`
|
|
2306
|
+
}]
|
|
2307
|
+
};
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2310
|
+
return {
|
|
2311
|
+
content: [{
|
|
2312
|
+
type: "text",
|
|
2313
|
+
text: `Error getting context health: ${errorMsg}`
|
|
2314
|
+
}]
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
1683
2318
|
case "bind_provider_session": {
|
|
1684
2319
|
const { providerSessionId, providerThreadId, replyChannel } = args;
|
|
1685
2320
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1686
|
-
const apiBaseUrl =
|
|
2321
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1687
2322
|
const orchestrationId = process.env.CMUX_ORCHESTRATION_ID;
|
|
1688
2323
|
const taskRunId = process.env.CMUX_TASK_RUN_ID;
|
|
1689
2324
|
if (!jwt) {
|
|
@@ -1746,7 +2381,7 @@ function createMemoryMcpServer(config) {
|
|
|
1746
2381
|
case "get_provider_session": {
|
|
1747
2382
|
const { taskId: providedTaskId } = args;
|
|
1748
2383
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1749
|
-
const apiBaseUrl =
|
|
2384
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1750
2385
|
const taskId = providedTaskId ?? process.env.CMUX_TASK_RUN_ID;
|
|
1751
2386
|
if (!jwt) {
|
|
1752
2387
|
return {
|
|
@@ -1824,7 +2459,7 @@ function createMemoryMcpServer(config) {
|
|
|
1824
2459
|
case "wait_for_events": {
|
|
1825
2460
|
const { orchestrationId, eventTypes = [], timeout = 3e4 } = args;
|
|
1826
2461
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1827
|
-
const apiBaseUrl =
|
|
2462
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1828
2463
|
if (!jwt) {
|
|
1829
2464
|
return {
|
|
1830
2465
|
content: [{
|
|
@@ -1939,7 +2574,7 @@ function createMemoryMcpServer(config) {
|
|
|
1939
2574
|
case "get_pending_approvals": {
|
|
1940
2575
|
const { orchestrationId: providedOrchId } = args;
|
|
1941
2576
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
1942
|
-
const apiBaseUrl =
|
|
2577
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
1943
2578
|
const orchestrationId = providedOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
|
|
1944
2579
|
if (!jwt) {
|
|
1945
2580
|
return {
|
|
@@ -1999,7 +2634,7 @@ function createMemoryMcpServer(config) {
|
|
|
1999
2634
|
case "resolve_approval": {
|
|
2000
2635
|
const { requestId, resolution, note } = args;
|
|
2001
2636
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
2002
|
-
const apiBaseUrl =
|
|
2637
|
+
const apiBaseUrl = getCmuxApiBaseUrl();
|
|
2003
2638
|
if (!jwt) {
|
|
2004
2639
|
return {
|
|
2005
2640
|
content: [{
|
|
@@ -2231,7 +2866,7 @@ function createMemoryMcpServer(config) {
|
|
|
2231
2866
|
}
|
|
2232
2867
|
case "log_learning": {
|
|
2233
2868
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
2234
|
-
const apiBase =
|
|
2869
|
+
const apiBase = getCmuxApiBaseUrl();
|
|
2235
2870
|
if (!jwt) {
|
|
2236
2871
|
return {
|
|
2237
2872
|
content: [{
|
|
@@ -2291,7 +2926,7 @@ function createMemoryMcpServer(config) {
|
|
|
2291
2926
|
}
|
|
2292
2927
|
case "get_active_orchestration_rules": {
|
|
2293
2928
|
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
2294
|
-
const apiBase =
|
|
2929
|
+
const apiBase = getCmuxApiBaseUrl();
|
|
2295
2930
|
if (!jwt) {
|
|
2296
2931
|
return {
|
|
2297
2932
|
content: [{
|
|
@@ -2359,6 +2994,242 @@ function createMemoryMcpServer(config) {
|
|
|
2359
2994
|
};
|
|
2360
2995
|
}
|
|
2361
2996
|
}
|
|
2997
|
+
// Operator Input Queue Tools (Active-Turn Steering)
|
|
2998
|
+
case "get_pending_operator_inputs": {
|
|
2999
|
+
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
3000
|
+
const apiBase = getCmuxApiBaseUrl();
|
|
3001
|
+
if (!jwt) {
|
|
3002
|
+
return {
|
|
3003
|
+
content: [{
|
|
3004
|
+
type: "text",
|
|
3005
|
+
text: "Error: CMUX_TASK_RUN_JWT not set. This tool requires authentication."
|
|
3006
|
+
}]
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
const { orchestrationId: inputOrchId } = args;
|
|
3010
|
+
const orchestrationId = inputOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
|
|
3011
|
+
if (!orchestrationId) {
|
|
3012
|
+
return {
|
|
3013
|
+
content: [{
|
|
3014
|
+
type: "text",
|
|
3015
|
+
text: "Error: No orchestration ID provided and CMUX_ORCHESTRATION_ID not set."
|
|
3016
|
+
}]
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
try {
|
|
3020
|
+
const response = await fetch(
|
|
3021
|
+
`${apiBase}/api/orchestrate/input/${orchestrationId}/status`,
|
|
3022
|
+
{
|
|
3023
|
+
method: "GET",
|
|
3024
|
+
headers: {
|
|
3025
|
+
"x-cmux-token": jwt
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
);
|
|
3029
|
+
if (!response.ok) {
|
|
3030
|
+
const errText = await response.text();
|
|
3031
|
+
return {
|
|
3032
|
+
content: [{
|
|
3033
|
+
type: "text",
|
|
3034
|
+
text: `Error getting queue status: ${response.status} ${errText}`
|
|
3035
|
+
}]
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
const status = await response.json();
|
|
3039
|
+
return {
|
|
3040
|
+
content: [{
|
|
3041
|
+
type: "text",
|
|
3042
|
+
text: JSON.stringify({
|
|
3043
|
+
hasPendingInputs: status.hasPendingInputs,
|
|
3044
|
+
depth: status.depth,
|
|
3045
|
+
capacity: status.capacity,
|
|
3046
|
+
oldestInputAt: status.oldestInputAt,
|
|
3047
|
+
message: status.hasPendingInputs ? `${status.depth} pending operator input(s). Call acknowledge_operator_inputs to process.` : "No pending operator inputs."
|
|
3048
|
+
}, null, 2)
|
|
3049
|
+
}]
|
|
3050
|
+
};
|
|
3051
|
+
} catch (error) {
|
|
3052
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3053
|
+
return {
|
|
3054
|
+
content: [{
|
|
3055
|
+
type: "text",
|
|
3056
|
+
text: `Error checking operator inputs: ${errorMsg}`
|
|
3057
|
+
}]
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
case "acknowledge_operator_inputs": {
|
|
3062
|
+
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
3063
|
+
const apiBase = getCmuxApiBaseUrl();
|
|
3064
|
+
if (!jwt) {
|
|
3065
|
+
return {
|
|
3066
|
+
content: [{
|
|
3067
|
+
type: "text",
|
|
3068
|
+
text: "Error: CMUX_TASK_RUN_JWT not set. This tool requires authentication."
|
|
3069
|
+
}]
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
const { orchestrationId: ackOrchId } = args;
|
|
3073
|
+
const orchestrationId = ackOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
|
|
3074
|
+
if (!orchestrationId) {
|
|
3075
|
+
return {
|
|
3076
|
+
content: [{
|
|
3077
|
+
type: "text",
|
|
3078
|
+
text: "Error: No orchestration ID provided and CMUX_ORCHESTRATION_ID not set."
|
|
3079
|
+
}]
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
try {
|
|
3083
|
+
const response = await fetch(
|
|
3084
|
+
`${apiBase}/api/orchestrate/input/${orchestrationId}/drain`,
|
|
3085
|
+
{
|
|
3086
|
+
method: "POST",
|
|
3087
|
+
headers: {
|
|
3088
|
+
"Content-Type": "application/json",
|
|
3089
|
+
"x-cmux-token": jwt
|
|
3090
|
+
},
|
|
3091
|
+
body: JSON.stringify({})
|
|
3092
|
+
}
|
|
3093
|
+
);
|
|
3094
|
+
if (!response.ok) {
|
|
3095
|
+
const errText = await response.text();
|
|
3096
|
+
return {
|
|
3097
|
+
content: [{
|
|
3098
|
+
type: "text",
|
|
3099
|
+
text: `Error draining operator inputs: ${response.status} ${errText}`
|
|
3100
|
+
}]
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
const result = await response.json();
|
|
3104
|
+
if (result.count === 0) {
|
|
3105
|
+
return {
|
|
3106
|
+
content: [{
|
|
3107
|
+
type: "text",
|
|
3108
|
+
text: JSON.stringify({
|
|
3109
|
+
count: 0,
|
|
3110
|
+
content: "",
|
|
3111
|
+
batchId: result.batchId,
|
|
3112
|
+
message: "No operator inputs to process."
|
|
3113
|
+
}, null, 2)
|
|
3114
|
+
}]
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
return {
|
|
3118
|
+
content: [{
|
|
3119
|
+
type: "text",
|
|
3120
|
+
text: JSON.stringify({
|
|
3121
|
+
count: result.count,
|
|
3122
|
+
content: result.content,
|
|
3123
|
+
batchId: result.batchId,
|
|
3124
|
+
inputIds: result.inputIds,
|
|
3125
|
+
message: `Processed ${result.count} operator input(s). Content merged above.`
|
|
3126
|
+
}, null, 2)
|
|
3127
|
+
}]
|
|
3128
|
+
};
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3131
|
+
return {
|
|
3132
|
+
content: [{
|
|
3133
|
+
type: "text",
|
|
3134
|
+
text: `Error acknowledging operator inputs: ${errorMsg}`
|
|
3135
|
+
}]
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
case "queue_operator_input": {
|
|
3140
|
+
const jwt = process.env.CMUX_TASK_RUN_JWT;
|
|
3141
|
+
const apiBase = getCmuxApiBaseUrl();
|
|
3142
|
+
if (!jwt) {
|
|
3143
|
+
return {
|
|
3144
|
+
content: [{
|
|
3145
|
+
type: "text",
|
|
3146
|
+
text: "Error: CMUX_TASK_RUN_JWT not set. This tool requires authentication."
|
|
3147
|
+
}]
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
const {
|
|
3151
|
+
orchestrationId: queueOrchId,
|
|
3152
|
+
content,
|
|
3153
|
+
priority
|
|
3154
|
+
} = args;
|
|
3155
|
+
const orchestrationId = queueOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
|
|
3156
|
+
if (!orchestrationId) {
|
|
3157
|
+
return {
|
|
3158
|
+
content: [{
|
|
3159
|
+
type: "text",
|
|
3160
|
+
text: "Error: No orchestration ID provided and CMUX_ORCHESTRATION_ID not set."
|
|
3161
|
+
}]
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
if (!content || content.trim().length === 0) {
|
|
3165
|
+
return {
|
|
3166
|
+
content: [{
|
|
3167
|
+
type: "text",
|
|
3168
|
+
text: "Error: Content is required."
|
|
3169
|
+
}]
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
try {
|
|
3173
|
+
const response = await fetch(
|
|
3174
|
+
`${apiBase}/api/orchestrate/input/${orchestrationId}`,
|
|
3175
|
+
{
|
|
3176
|
+
method: "POST",
|
|
3177
|
+
headers: {
|
|
3178
|
+
"Content-Type": "application/json",
|
|
3179
|
+
"x-cmux-token": jwt
|
|
3180
|
+
},
|
|
3181
|
+
body: JSON.stringify({
|
|
3182
|
+
content: content.trim(),
|
|
3183
|
+
priority: priority ?? "normal"
|
|
3184
|
+
})
|
|
3185
|
+
}
|
|
3186
|
+
);
|
|
3187
|
+
if (!response.ok) {
|
|
3188
|
+
const errText = await response.text();
|
|
3189
|
+
return {
|
|
3190
|
+
content: [{
|
|
3191
|
+
type: "text",
|
|
3192
|
+
text: `Error queuing input: ${response.status} ${errText}`
|
|
3193
|
+
}]
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
const result = await response.json();
|
|
3197
|
+
if (!result.success) {
|
|
3198
|
+
return {
|
|
3199
|
+
content: [{
|
|
3200
|
+
type: "text",
|
|
3201
|
+
text: JSON.stringify({
|
|
3202
|
+
queued: false,
|
|
3203
|
+
error: result.error,
|
|
3204
|
+
queueDepth: result.queueDepth,
|
|
3205
|
+
queueCapacity: result.queueCapacity,
|
|
3206
|
+
message: `Queue is full (${result.queueDepth}/${result.queueCapacity}). Wait for agent to drain before sending more.`
|
|
3207
|
+
}, null, 2)
|
|
3208
|
+
}]
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
return {
|
|
3212
|
+
content: [{
|
|
3213
|
+
type: "text",
|
|
3214
|
+
text: JSON.stringify({
|
|
3215
|
+
queued: true,
|
|
3216
|
+
inputId: result.inputId,
|
|
3217
|
+
queueDepth: result.queueDepth,
|
|
3218
|
+
priority: priority ?? "normal",
|
|
3219
|
+
message: `Input queued successfully (${result.queueDepth} in queue).`
|
|
3220
|
+
}, null, 2)
|
|
3221
|
+
}]
|
|
3222
|
+
};
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3225
|
+
return {
|
|
3226
|
+
content: [{
|
|
3227
|
+
type: "text",
|
|
3228
|
+
text: `Error queuing operator input: ${errorMsg}`
|
|
3229
|
+
}]
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
2362
3233
|
default:
|
|
2363
3234
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
2364
3235
|
}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devsh-memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP server for devsh/cmux agent memory - enables Claude Desktop and external clients to access sandbox memory and orchestrate multi-agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|