devsh-memory-mcp 0.3.8 → 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 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.sh) |
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;
@@ -184,7 +495,7 @@ function createMemoryMcpServer(config) {
184
495
  tools: [
185
496
  {
186
497
  name: "read_memory",
187
- description: 'Read a memory file. Type can be "knowledge", "tasks", or "mailbox".',
498
+ description: 'Read a memory file. Type can be "knowledge", "tasks", or "mailbox". For tasks, by default only returns open tasks (pending/in_progress) to prevent context bloat.',
188
499
  inputSchema: {
189
500
  type: "object",
190
501
  properties: {
@@ -192,6 +503,14 @@ function createMemoryMcpServer(config) {
192
503
  type: "string",
193
504
  enum: ["knowledge", "tasks", "mailbox"],
194
505
  description: "The type of memory to read"
506
+ },
507
+ includeCompleted: {
508
+ type: "boolean",
509
+ description: "For tasks: include completed tasks (default: false, only open tasks)"
510
+ },
511
+ limit: {
512
+ type: "number",
513
+ description: "For tasks: maximum number of tasks to return (default: 50)"
195
514
  }
196
515
  },
197
516
  required: ["type"]
@@ -233,9 +552,34 @@ function createMemoryMcpServer(config) {
233
552
  required: ["query"]
234
553
  }
235
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
+ },
236
580
  {
237
581
  name: "send_message",
238
- description: 'Send a message to another agent on the same task. Use "*" to broadcast to all agents.',
582
+ description: 'Send a message to another agent on the same task. Use "*" to broadcast to all agents. Aligned with Claude Code SendMessage pattern.',
239
583
  inputSchema: {
240
584
  type: "object",
241
585
  properties: {
@@ -249,8 +593,25 @@ function createMemoryMcpServer(config) {
249
593
  },
250
594
  type: {
251
595
  type: "string",
252
- enum: ["handoff", "request", "status"],
253
- description: "Message type: handoff (work transfer), request (ask to do something), status (progress update)"
596
+ enum: ["handoff", "request", "status", "response"],
597
+ description: "Message type: handoff (work transfer), request (ask to do something), status (progress update), response (reply to a request)"
598
+ },
599
+ correlationId: {
600
+ type: "string",
601
+ description: "Optional correlation ID for request-response tracking. Use to link related messages."
602
+ },
603
+ replyTo: {
604
+ type: "string",
605
+ description: "Optional message ID this is responding to (for response type messages)."
606
+ },
607
+ priority: {
608
+ type: "string",
609
+ enum: ["high", "normal", "low"],
610
+ description: "Message priority (default: normal). High priority messages appear first."
611
+ },
612
+ metadata: {
613
+ type: "object",
614
+ description: "Optional additional context as key-value pairs."
254
615
  }
255
616
  },
256
617
  required: ["to", "message"]
@@ -258,13 +619,22 @@ function createMemoryMcpServer(config) {
258
619
  },
259
620
  {
260
621
  name: "get_my_messages",
261
- description: "Get all messages addressed to this agent (including broadcasts). Returns unread messages first.",
622
+ description: "Get all messages addressed to this agent (including broadcasts). Returns high-priority and unread messages first.",
262
623
  inputSchema: {
263
624
  type: "object",
264
625
  properties: {
265
626
  includeRead: {
266
627
  type: "boolean",
267
628
  description: "Include messages already marked as read (default: false)"
629
+ },
630
+ correlationId: {
631
+ type: "string",
632
+ description: "Filter messages by correlation ID (for tracking request-response chains)"
633
+ },
634
+ type: {
635
+ type: "string",
636
+ enum: ["handoff", "request", "status", "response"],
637
+ description: "Filter messages by type"
268
638
  }
269
639
  }
270
640
  }
@@ -569,6 +939,14 @@ function createMemoryMcpServer(config) {
569
939
  properties: {}
570
940
  }
571
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
+ },
572
950
  {
573
951
  name: "bind_provider_session",
574
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.",
@@ -716,6 +1094,56 @@ function createMemoryMcpServer(config) {
716
1094
  }
717
1095
  }
718
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
+ }
719
1147
  }
720
1148
  ]
721
1149
  }));
@@ -723,12 +1151,35 @@ function createMemoryMcpServer(config) {
723
1151
  const { name, arguments: args } = request.params;
724
1152
  switch (name) {
725
1153
  case "read_memory": {
726
- const type = args.type;
1154
+ const { type, includeCompleted, limit } = args;
1155
+ trackRead(type);
727
1156
  let content = null;
728
1157
  if (type === "knowledge") {
729
1158
  content = readFile(path.join(knowledgeDir, "MEMORY.md"));
730
1159
  } else if (type === "tasks") {
731
- content = readFile(tasksPath);
1160
+ const tasksFile = readTasks();
1161
+ const maxTasks = limit ?? 50;
1162
+ const showCompleted = includeCompleted ?? false;
1163
+ let filteredTasks = showCompleted ? tasksFile.tasks : tasksFile.tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
1164
+ filteredTasks = filteredTasks.sort((a, b) => {
1165
+ const aNum = parseInt(a.id.replace(/\D/g, ""), 10) || 0;
1166
+ const bNum = parseInt(b.id.replace(/\D/g, ""), 10) || 0;
1167
+ return bNum - aNum;
1168
+ });
1169
+ const truncated = filteredTasks.length > maxTasks;
1170
+ filteredTasks = filteredTasks.slice(0, maxTasks);
1171
+ const result = {
1172
+ version: tasksFile.version,
1173
+ tasks: filteredTasks,
1174
+ _meta: {
1175
+ totalTasks: tasksFile.tasks.length,
1176
+ openTasks: tasksFile.tasks.filter((t) => t.status === "pending" || t.status === "in_progress").length,
1177
+ returnedTasks: filteredTasks.length,
1178
+ truncated,
1179
+ includeCompleted: showCompleted
1180
+ }
1181
+ };
1182
+ content = JSON.stringify(result, null, 2);
732
1183
  } else if (type === "mailbox") {
733
1184
  content = readFile(mailboxPath);
734
1185
  }
@@ -758,8 +1209,122 @@ function createMemoryMcpServer(config) {
758
1209
  const formatted = results.map((r) => `[${r.source}${r.line ? `:${r.line}` : ""}] ${r.content}`).join("\n");
759
1210
  return { content: [{ type: "text", text: formatted }] };
760
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
+ }
761
1326
  case "send_message": {
762
- const { to, message, type } = args;
1327
+ const { to, message, type, correlationId, replyTo, priority, metadata } = args;
763
1328
  const mailbox = readMailbox();
764
1329
  const newMessage = {
765
1330
  id: generateMessageId(),
@@ -768,23 +1333,48 @@ function createMemoryMcpServer(config) {
768
1333
  type: type ?? "request",
769
1334
  message,
770
1335
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
771
- read: false
1336
+ read: false,
1337
+ correlationId,
1338
+ replyTo,
1339
+ priority,
1340
+ metadata
772
1341
  };
773
1342
  mailbox.messages.push(newMessage);
774
1343
  writeMailbox(mailbox);
775
- return { content: [{ type: "text", text: `Message sent successfully. ID: ${newMessage.id}` }] };
1344
+ scheduleSyncForFile("send_message");
1345
+ const responseText = correlationId ? `Message sent successfully. ID: ${newMessage.id}, correlationId: ${correlationId}` : `Message sent successfully. ID: ${newMessage.id}`;
1346
+ return { content: [{ type: "text", text: responseText }] };
776
1347
  }
777
1348
  case "get_my_messages": {
778
- const includeRead = args.includeRead ?? false;
1349
+ const { includeRead, correlationId, type: filterType } = args;
779
1350
  const mailbox = readMailbox();
780
- const myMessages = mailbox.messages.filter(
1351
+ let myMessages = mailbox.messages.filter(
781
1352
  (m) => m.to === agentName || m.to === "*"
782
1353
  );
783
- const filtered = includeRead ? myMessages : myMessages.filter((m) => !m.read);
784
- if (filtered.length === 0) {
1354
+ if (!includeRead) {
1355
+ myMessages = myMessages.filter((m) => !m.read);
1356
+ }
1357
+ if (correlationId) {
1358
+ myMessages = myMessages.filter((m) => m.correlationId === correlationId);
1359
+ }
1360
+ if (filterType) {
1361
+ myMessages = myMessages.filter((m) => m.type === filterType);
1362
+ }
1363
+ const priorityOrder = { high: 0, normal: 1, low: 2, undefined: 1 };
1364
+ myMessages.sort((a, b) => {
1365
+ const pa = priorityOrder[a.priority ?? "normal"];
1366
+ const pb = priorityOrder[b.priority ?? "normal"];
1367
+ if (pa !== pb) return pa - pb;
1368
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
1369
+ });
1370
+ if (myMessages.length === 0) {
785
1371
  return { content: [{ type: "text", text: "No messages for you." }] };
786
1372
  }
787
- const formatted = filtered.map((m) => `[${m.id}] ${m.type ?? "message"} from ${m.from}: ${m.message}`).join("\n\n");
1373
+ const formatted = myMessages.map((m) => {
1374
+ const priorityTag = m.priority === "high" ? "[HIGH] " : m.priority === "low" ? "[low] " : "";
1375
+ const corrTag = m.correlationId ? ` (corr: ${m.correlationId})` : "";
1376
+ return `${priorityTag}[${m.id}] ${m.type ?? "message"} from ${m.from}${corrTag}: ${m.message}`;
1377
+ }).join("\n\n");
788
1378
  return { content: [{ type: "text", text: formatted }] };
789
1379
  }
790
1380
  case "mark_read": {
@@ -796,6 +1386,7 @@ function createMemoryMcpServer(config) {
796
1386
  }
797
1387
  message.read = true;
798
1388
  writeMailbox(mailbox);
1389
+ scheduleSyncForFile("mark_read");
799
1390
  return { content: [{ type: "text", text: `Message ${messageId} marked as read.` }] };
800
1391
  }
801
1392
  // Write tool handlers
@@ -814,12 +1405,25 @@ function createMemoryMcpServer(config) {
814
1405
  const newContent = existing + `
815
1406
  - [${timestamp}] ${content}`;
816
1407
  if (writeFile(logPath, newContent)) {
1408
+ scheduleSyncForFile("append_daily_log");
817
1409
  return { content: [{ type: "text", text: `Appended to daily/${today}.md` }] };
818
1410
  }
819
1411
  return { content: [{ type: "text", text: `Failed to append to daily log` }] };
820
1412
  }
821
1413
  case "update_knowledge": {
822
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}`);
823
1427
  ensureDir(knowledgeDir);
824
1428
  const knowledgePath = path.join(knowledgeDir, "MEMORY.md");
825
1429
  let existing = readFile(knowledgePath);
@@ -854,6 +1458,45 @@ function createMemoryMcpServer(config) {
854
1458
  if (headerIndex === -1) {
855
1459
  return { content: [{ type: "text", text: `Section ${section} not found in MEMORY.md` }] };
856
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
+ }
857
1500
  const afterHeader = existing.slice(headerIndex + header.length);
858
1501
  const nextSectionMatch = afterHeader.match(/\n## /);
859
1502
  const insertPoint = nextSectionMatch ? headerIndex + header.length + (nextSectionMatch.index ?? afterHeader.length) : existing.length;
@@ -862,6 +1505,7 @@ function createMemoryMcpServer(config) {
862
1505
  const actualInsertPoint = Math.min(commentEnd, insertPoint);
863
1506
  const updated = existing.slice(0, actualInsertPoint) + newEntry + "\n" + existing.slice(actualInsertPoint);
864
1507
  if (writeFile(knowledgePath, updated)) {
1508
+ scheduleSyncForFile("update_knowledge");
865
1509
  return { content: [{ type: "text", text: `Added entry to ${section} section in MEMORY.md` }] };
866
1510
  }
867
1511
  return { content: [{ type: "text", text: `Failed to update MEMORY.md` }] };
@@ -880,6 +1524,7 @@ function createMemoryMcpServer(config) {
880
1524
  };
881
1525
  tasks.tasks.push(newTask);
882
1526
  if (writeTasks(tasks)) {
1527
+ scheduleSyncForFile("add_task");
883
1528
  return { content: [{ type: "text", text: `Task created with ID: ${newTask.id}` }] };
884
1529
  }
885
1530
  return { content: [{ type: "text", text: `Failed to create task` }] };
@@ -894,6 +1539,7 @@ function createMemoryMcpServer(config) {
894
1539
  task.status = status;
895
1540
  task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
896
1541
  if (writeTasks(tasks)) {
1542
+ scheduleSyncForFile("update_task");
897
1543
  return { content: [{ type: "text", text: `Task ${taskId} updated to status: ${status}` }] };
898
1544
  }
899
1545
  return { content: [{ type: "text", text: `Failed to update task` }] };
@@ -923,6 +1569,7 @@ function createMemoryMcpServer(config) {
923
1569
  if (agentName2) eventObj.agentName = agentName2;
924
1570
  if (taskRunId) eventObj.taskRunId = taskRunId;
925
1571
  if (appendEvent(eventObj)) {
1572
+ scheduleSyncForFile("append_event");
926
1573
  return { content: [{ type: "text", text: `Event appended to EVENTS.jsonl` }] };
927
1574
  }
928
1575
  return { content: [{ type: "text", text: `Failed to append event` }] };
@@ -947,6 +1594,7 @@ function createMemoryMcpServer(config) {
947
1594
  task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
948
1595
  }
949
1596
  if (writePlan(plan)) {
1597
+ scheduleSyncForFile("update_plan_task");
950
1598
  return { content: [{ type: "text", text: `Plan task ${taskId} updated to status: ${status}` }] };
951
1599
  }
952
1600
  return { content: [{ type: "text", text: `Failed to update plan task` }] };
@@ -955,7 +1603,7 @@ function createMemoryMcpServer(config) {
955
1603
  const { orchestrationId: argOrchId } = args;
956
1604
  const orchestrationId = argOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
957
1605
  const jwt = process.env.CMUX_TASK_RUN_JWT;
958
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
1606
+ const apiBaseUrl = getCmuxApiBaseUrl();
959
1607
  if (!orchestrationId) {
960
1608
  return {
961
1609
  content: [{
@@ -1066,7 +1714,7 @@ function createMemoryMcpServer(config) {
1066
1714
  const { orchestrationId: argOrchId, headAgentStatus, message, taskIds } = args;
1067
1715
  const orchestrationId = argOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
1068
1716
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1069
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
1717
+ const apiBaseUrl = getCmuxApiBaseUrl();
1070
1718
  if (!orchestrationId) {
1071
1719
  return {
1072
1720
  content: [{
@@ -1403,7 +2051,7 @@ function createMemoryMcpServer(config) {
1403
2051
  case "list_spawned_agents": {
1404
2052
  const { status: filterStatus } = args;
1405
2053
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1406
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2054
+ const apiBaseUrl = getCmuxApiBaseUrl();
1407
2055
  const orchestrationId = process.env.CMUX_ORCHESTRATION_ID;
1408
2056
  if (!jwt) {
1409
2057
  return {
@@ -1462,7 +2110,7 @@ function createMemoryMcpServer(config) {
1462
2110
  case "cancel_agent": {
1463
2111
  const { orchestrationTaskId, cascade = false } = args;
1464
2112
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1465
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2113
+ const apiBaseUrl = getCmuxApiBaseUrl();
1466
2114
  if (!jwt) {
1467
2115
  return {
1468
2116
  content: [{
@@ -1530,7 +2178,7 @@ function createMemoryMcpServer(config) {
1530
2178
  }
1531
2179
  case "get_orchestration_summary": {
1532
2180
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1533
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2181
+ const apiBaseUrl = getCmuxApiBaseUrl();
1534
2182
  const orchestrationId = process.env.CMUX_ORCHESTRATION_ID;
1535
2183
  if (!jwt) {
1536
2184
  return {
@@ -1600,10 +2248,77 @@ function createMemoryMcpServer(config) {
1600
2248
  };
1601
2249
  }
1602
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
+ }
1603
2318
  case "bind_provider_session": {
1604
2319
  const { providerSessionId, providerThreadId, replyChannel } = args;
1605
2320
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1606
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2321
+ const apiBaseUrl = getCmuxApiBaseUrl();
1607
2322
  const orchestrationId = process.env.CMUX_ORCHESTRATION_ID;
1608
2323
  const taskRunId = process.env.CMUX_TASK_RUN_ID;
1609
2324
  if (!jwt) {
@@ -1666,7 +2381,7 @@ function createMemoryMcpServer(config) {
1666
2381
  case "get_provider_session": {
1667
2382
  const { taskId: providedTaskId } = args;
1668
2383
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1669
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2384
+ const apiBaseUrl = getCmuxApiBaseUrl();
1670
2385
  const taskId = providedTaskId ?? process.env.CMUX_TASK_RUN_ID;
1671
2386
  if (!jwt) {
1672
2387
  return {
@@ -1744,7 +2459,7 @@ function createMemoryMcpServer(config) {
1744
2459
  case "wait_for_events": {
1745
2460
  const { orchestrationId, eventTypes = [], timeout = 3e4 } = args;
1746
2461
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1747
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2462
+ const apiBaseUrl = getCmuxApiBaseUrl();
1748
2463
  if (!jwt) {
1749
2464
  return {
1750
2465
  content: [{
@@ -1859,7 +2574,7 @@ function createMemoryMcpServer(config) {
1859
2574
  case "get_pending_approvals": {
1860
2575
  const { orchestrationId: providedOrchId } = args;
1861
2576
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1862
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2577
+ const apiBaseUrl = getCmuxApiBaseUrl();
1863
2578
  const orchestrationId = providedOrchId ?? process.env.CMUX_ORCHESTRATION_ID;
1864
2579
  if (!jwt) {
1865
2580
  return {
@@ -1919,7 +2634,7 @@ function createMemoryMcpServer(config) {
1919
2634
  case "resolve_approval": {
1920
2635
  const { requestId, resolution, note } = args;
1921
2636
  const jwt = process.env.CMUX_TASK_RUN_JWT;
1922
- const apiBaseUrl = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2637
+ const apiBaseUrl = getCmuxApiBaseUrl();
1923
2638
  if (!jwt) {
1924
2639
  return {
1925
2640
  content: [{
@@ -2151,7 +2866,7 @@ function createMemoryMcpServer(config) {
2151
2866
  }
2152
2867
  case "log_learning": {
2153
2868
  const jwt = process.env.CMUX_TASK_RUN_JWT;
2154
- const apiBase = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2869
+ const apiBase = getCmuxApiBaseUrl();
2155
2870
  if (!jwt) {
2156
2871
  return {
2157
2872
  content: [{
@@ -2211,7 +2926,7 @@ function createMemoryMcpServer(config) {
2211
2926
  }
2212
2927
  case "get_active_orchestration_rules": {
2213
2928
  const jwt = process.env.CMUX_TASK_RUN_JWT;
2214
- const apiBase = process.env.CMUX_API_BASE_URL ?? "https://cmux.sh";
2929
+ const apiBase = getCmuxApiBaseUrl();
2215
2930
  if (!jwt) {
2216
2931
  return {
2217
2932
  content: [{
@@ -2279,6 +2994,242 @@ function createMemoryMcpServer(config) {
2279
2994
  };
2280
2995
  }
2281
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
+ }
2282
3233
  default:
2283
3234
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
2284
3235
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runServer
4
- } from "./chunk-BKOJ5TNO.js";
4
+ } from "./chunk-R25AQROL.js";
5
5
 
6
6
  // src/cli.ts
7
7
  function parseArgs() {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMemoryMcpServer,
3
3
  runServer
4
- } from "./chunk-BKOJ5TNO.js";
4
+ } from "./chunk-R25AQROL.js";
5
5
  export {
6
6
  createMemoryMcpServer,
7
7
  runServer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devsh-memory-mcp",
3
- "version": "0.3.8",
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",
@@ -11,6 +11,7 @@
11
11
  "scripts": {
12
12
  "build": "tsup",
13
13
  "dev": "tsup --watch",
14
+ "test": "bun test",
14
15
  "typecheck": "tsc --noEmit",
15
16
  "prepublishOnly": "npm run build"
16
17
  },