agent-coord-mcp 0.4.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "File-backed MCP server for coordinating multiple AI coding agents (Claude Code, Cursor, Cline, etc.) on the same machine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -429,23 +429,92 @@ async function postStatus(status) {
429
429
  async function pruneOld(days) {
430
430
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
431
431
  let total = 0;
432
- const files = [ROOM_FILE, STATUS_FILE_PATH];
432
+ // Per-file removal counts so we can shift the corresponding cursor
433
+ // offsets — without this, every other agent's roomOffset/statusOffset/
434
+ // inboxOffset would point past the now-shorter file and they'd silently
435
+ // miss messages until enough new ones piled up to overtake the stale offset.
436
+ let roomRemoved = 0;
437
+ let statusRemoved = 0;
438
+ const inboxRemoved = {}; // agentId → count
439
+
440
+ const files = [
441
+ { path: ROOM_FILE, kind: "room" },
442
+ { path: STATUS_FILE_PATH, kind: "status" },
443
+ ];
433
444
  if (existsSync(INBOX_DIR)) {
434
445
  for (const n of readdirSync(INBOX_DIR)) {
435
- if (n.endsWith(".jsonl")) files.push(path.join(INBOX_DIR, n));
446
+ if (n.endsWith(".jsonl")) {
447
+ files.push({ path: path.join(INBOX_DIR, n), kind: "inbox", agentId: n.replace(/\.jsonl$/, "") });
448
+ }
436
449
  }
437
450
  }
438
- for (const file of files) {
439
- if (!existsSync(file)) continue;
440
- const all = readJsonl(file);
451
+ for (const f of files) {
452
+ if (!existsSync(f.path)) continue;
453
+ const all = readJsonl(f.path);
441
454
  const kept = all.filter((e) => e && e.ts > cutoff);
442
- if (kept.length < all.length) {
455
+ const removed = all.length - kept.length;
456
+ if (removed > 0) {
443
457
  const body = kept.length ? kept.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
444
- writeFileSync(file, body);
445
- total += all.length - kept.length;
458
+ writeFileSync(f.path, body);
459
+ total += removed;
460
+ if (f.kind === "room") roomRemoved += removed;
461
+ else if (f.kind === "status") statusRemoved += removed;
462
+ else inboxRemoved[f.agentId] = (inboxRemoved[f.agentId] ?? 0) + removed;
463
+ }
464
+ }
465
+ if (roomRemoved || statusRemoved || Object.keys(inboxRemoved).length) {
466
+ shiftAllCursors({ roomRemoved, statusRemoved, inboxRemoved });
467
+ }
468
+ say(A.dim(`→ pruned ${total} entries older than ${days}d (cursors adjusted)`));
469
+ }
470
+
471
+ async function wipeRoom() {
472
+ await ensureFile(ROOM_FILE);
473
+ writeFileSync(ROOM_FILE, "");
474
+ // Reset every agent's roomOffset to 0 so they start reading the (now empty)
475
+ // file from the beginning. Otherwise their stale offsets point past EOF.
476
+ resetAllRoomOffsets();
477
+ say(A.dim("→ room wiped (all room cursors reset)"));
478
+ }
479
+
480
+ // Walk every cursor file and shift offsets down by the per-channel removed
481
+ // counts. Mirrors what the MCP prune tool does server-side.
482
+ function shiftAllCursors({ roomRemoved = 0, statusRemoved = 0, inboxRemoved = {} }) {
483
+ if (!existsSync(CURSOR_DIR)) return;
484
+ for (const name of readdirSync(CURSOR_DIR)) {
485
+ if (!name.endsWith(".json")) continue;
486
+ const cursorPath = path.join(CURSOR_DIR, name);
487
+ const cur = readJsonSafe(cursorPath, {});
488
+ const id = name.replace(/\.json$/, "");
489
+ let touched = false;
490
+ if (cur.roomOffset !== undefined && roomRemoved > 0) {
491
+ cur.roomOffset = Math.max(0, cur.roomOffset - roomRemoved);
492
+ touched = true;
493
+ }
494
+ if (cur.statusOffset !== undefined && statusRemoved > 0) {
495
+ cur.statusOffset = Math.max(0, cur.statusOffset - statusRemoved);
496
+ touched = true;
497
+ }
498
+ const myInbox = inboxRemoved[id] ?? 0;
499
+ if (cur.inboxOffset !== undefined && myInbox > 0) {
500
+ cur.inboxOffset = Math.max(0, cur.inboxOffset - myInbox);
501
+ touched = true;
502
+ }
503
+ if (touched) writeJsonAtomic(cursorPath, cur);
504
+ }
505
+ }
506
+
507
+ function resetAllRoomOffsets() {
508
+ if (!existsSync(CURSOR_DIR)) return;
509
+ for (const name of readdirSync(CURSOR_DIR)) {
510
+ if (!name.endsWith(".json")) continue;
511
+ const cursorPath = path.join(CURSOR_DIR, name);
512
+ const cur = readJsonSafe(cursorPath, {});
513
+ if (cur.roomOffset !== undefined && cur.roomOffset !== 0) {
514
+ cur.roomOffset = 0;
515
+ writeJsonAtomic(cursorPath, cur);
446
516
  }
447
517
  }
448
- say(A.dim(`→ pruned ${total} entries older than ${days}d`));
449
518
  }
450
519
 
451
520
  async function kickAgent(target) {
@@ -471,19 +540,13 @@ async function kickAgent(target) {
471
540
  try { process.kill(marker.pid, "SIGTERM"); } catch {}
472
541
  }
473
542
  try { if (existsSync(markerPath)) unlinkSync(markerPath); } catch {}
474
- say(A.dim(`→ kicked ${target} (registry + transport cleared)`));
475
- }
476
-
477
- async function wipeRoom() {
478
- await ensureFile(ROOM_FILE);
479
- writeFileSync(ROOM_FILE, "");
480
- // Reset cursors so prior offsets don't point past the now-shorter file.
481
- const cur = readJsonSafe(CURSOR_FILE, {});
482
- if (cur.roomOffset !== undefined) {
483
- delete cur.roomOffset;
484
- writeJsonAtomic(CURSOR_FILE, cur);
485
- }
486
- say(A.dim("→ room wiped"));
543
+ // Remove the kicked agent's inbox + cursor so they don't sit orphaned in
544
+ // ~/agent-coord/ taking up listing space and confusing future bookkeeping.
545
+ const inboxPath = path.join(INBOX_DIR, `${sanitize(target)}.jsonl`);
546
+ const cursorPath = path.join(CURSOR_DIR, `${sanitize(target)}.json`);
547
+ try { if (existsSync(inboxPath)) unlinkSync(inboxPath); } catch {}
548
+ try { if (existsSync(cursorPath)) unlinkSync(cursorPath); } catch {}
549
+ say(A.dim(`→ kicked ${target} (registry, transport, inbox, cursor all cleared)`));
487
550
  }
488
551
 
489
552
  async function findInHistory(term) {