agent-coord-mcp 0.4.0 → 0.4.3

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.3",
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": {
@@ -67,11 +67,19 @@ const A = {
67
67
  blue: (s) => `\x1b[34m${s}\x1b[0m`,
68
68
  magenta: (s) => `\x1b[35m${s}\x1b[0m`,
69
69
  cyan: (s) => `\x1b[36m${s}\x1b[0m`,
70
+ brightGreen: (s) => `\x1b[92m${s}\x1b[0m`,
71
+ brightYellow: (s) => `\x1b[93m${s}\x1b[0m`,
72
+ brightBlue: (s) => `\x1b[94m${s}\x1b[0m`,
73
+ brightMagenta: (s) => `\x1b[95m${s}\x1b[0m`,
74
+ brightCyan: (s) => `\x1b[96m${s}\x1b[0m`,
70
75
  };
71
76
 
72
- // Stable per-agent color from agentId hash. Skip red (reserved for errors)
73
- // and white/black; cycle the remaining 5 bright colors.
74
- const AGENT_COLORS = [A.green, A.yellow, A.blue, A.magenta, A.cyan];
77
+ // Stable per-agent color from agentId hash. 10 colors (standard + bright)
78
+ // to reduce collisions. Red is reserved for errors so we skip it.
79
+ const AGENT_COLORS = [
80
+ A.green, A.yellow, A.blue, A.magenta, A.cyan,
81
+ A.brightGreen, A.brightYellow, A.brightBlue, A.brightMagenta, A.brightCyan,
82
+ ];
75
83
  function agentColor(id) {
76
84
  let h = 0;
77
85
  for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
@@ -429,23 +437,92 @@ async function postStatus(status) {
429
437
  async function pruneOld(days) {
430
438
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
431
439
  let total = 0;
432
- const files = [ROOM_FILE, STATUS_FILE_PATH];
440
+ // Per-file removal counts so we can shift the corresponding cursor
441
+ // offsets — without this, every other agent's roomOffset/statusOffset/
442
+ // inboxOffset would point past the now-shorter file and they'd silently
443
+ // miss messages until enough new ones piled up to overtake the stale offset.
444
+ let roomRemoved = 0;
445
+ let statusRemoved = 0;
446
+ const inboxRemoved = {}; // agentId → count
447
+
448
+ const files = [
449
+ { path: ROOM_FILE, kind: "room" },
450
+ { path: STATUS_FILE_PATH, kind: "status" },
451
+ ];
433
452
  if (existsSync(INBOX_DIR)) {
434
453
  for (const n of readdirSync(INBOX_DIR)) {
435
- if (n.endsWith(".jsonl")) files.push(path.join(INBOX_DIR, n));
454
+ if (n.endsWith(".jsonl")) {
455
+ files.push({ path: path.join(INBOX_DIR, n), kind: "inbox", agentId: n.replace(/\.jsonl$/, "") });
456
+ }
436
457
  }
437
458
  }
438
- for (const file of files) {
439
- if (!existsSync(file)) continue;
440
- const all = readJsonl(file);
459
+ for (const f of files) {
460
+ if (!existsSync(f.path)) continue;
461
+ const all = readJsonl(f.path);
441
462
  const kept = all.filter((e) => e && e.ts > cutoff);
442
- if (kept.length < all.length) {
463
+ const removed = all.length - kept.length;
464
+ if (removed > 0) {
443
465
  const body = kept.length ? kept.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
444
- writeFileSync(file, body);
445
- total += all.length - kept.length;
466
+ writeFileSync(f.path, body);
467
+ total += removed;
468
+ if (f.kind === "room") roomRemoved += removed;
469
+ else if (f.kind === "status") statusRemoved += removed;
470
+ else inboxRemoved[f.agentId] = (inboxRemoved[f.agentId] ?? 0) + removed;
471
+ }
472
+ }
473
+ if (roomRemoved || statusRemoved || Object.keys(inboxRemoved).length) {
474
+ shiftAllCursors({ roomRemoved, statusRemoved, inboxRemoved });
475
+ }
476
+ say(A.dim(`→ pruned ${total} entries older than ${days}d (cursors adjusted)`));
477
+ }
478
+
479
+ async function wipeRoom() {
480
+ await ensureFile(ROOM_FILE);
481
+ writeFileSync(ROOM_FILE, "");
482
+ // Reset every agent's roomOffset to 0 so they start reading the (now empty)
483
+ // file from the beginning. Otherwise their stale offsets point past EOF.
484
+ resetAllRoomOffsets();
485
+ say(A.dim("→ room wiped (all room cursors reset)"));
486
+ }
487
+
488
+ // Walk every cursor file and shift offsets down by the per-channel removed
489
+ // counts. Mirrors what the MCP prune tool does server-side.
490
+ function shiftAllCursors({ roomRemoved = 0, statusRemoved = 0, inboxRemoved = {} }) {
491
+ if (!existsSync(CURSOR_DIR)) return;
492
+ for (const name of readdirSync(CURSOR_DIR)) {
493
+ if (!name.endsWith(".json")) continue;
494
+ const cursorPath = path.join(CURSOR_DIR, name);
495
+ const cur = readJsonSafe(cursorPath, {});
496
+ const id = name.replace(/\.json$/, "");
497
+ let touched = false;
498
+ if (cur.roomOffset !== undefined && roomRemoved > 0) {
499
+ cur.roomOffset = Math.max(0, cur.roomOffset - roomRemoved);
500
+ touched = true;
501
+ }
502
+ if (cur.statusOffset !== undefined && statusRemoved > 0) {
503
+ cur.statusOffset = Math.max(0, cur.statusOffset - statusRemoved);
504
+ touched = true;
505
+ }
506
+ const myInbox = inboxRemoved[id] ?? 0;
507
+ if (cur.inboxOffset !== undefined && myInbox > 0) {
508
+ cur.inboxOffset = Math.max(0, cur.inboxOffset - myInbox);
509
+ touched = true;
510
+ }
511
+ if (touched) writeJsonAtomic(cursorPath, cur);
512
+ }
513
+ }
514
+
515
+ function resetAllRoomOffsets() {
516
+ if (!existsSync(CURSOR_DIR)) return;
517
+ for (const name of readdirSync(CURSOR_DIR)) {
518
+ if (!name.endsWith(".json")) continue;
519
+ const cursorPath = path.join(CURSOR_DIR, name);
520
+ const cur = readJsonSafe(cursorPath, {});
521
+ if (cur.roomOffset !== undefined && cur.roomOffset !== 0) {
522
+ cur.roomOffset = 0;
523
+ writeJsonAtomic(cursorPath, cur);
446
524
  }
447
525
  }
448
- say(A.dim(`→ pruned ${total} entries older than ${days}d`));
449
526
  }
450
527
 
451
528
  async function kickAgent(target) {
@@ -471,19 +548,13 @@ async function kickAgent(target) {
471
548
  try { process.kill(marker.pid, "SIGTERM"); } catch {}
472
549
  }
473
550
  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"));
551
+ // Remove the kicked agent's inbox + cursor so they don't sit orphaned in
552
+ // ~/agent-coord/ taking up listing space and confusing future bookkeeping.
553
+ const inboxPath = path.join(INBOX_DIR, `${sanitize(target)}.jsonl`);
554
+ const cursorPath = path.join(CURSOR_DIR, `${sanitize(target)}.json`);
555
+ try { if (existsSync(inboxPath)) unlinkSync(inboxPath); } catch {}
556
+ try { if (existsSync(cursorPath)) unlinkSync(cursorPath); } catch {}
557
+ say(A.dim(`→ kicked ${target} (registry, transport, inbox, cursor all cleared)`));
487
558
  }
488
559
 
489
560
  async function findInHistory(term) {
@@ -541,19 +612,51 @@ function printMsg(kind, m, opts = {}) {
541
612
  const d = new Date(m.ts);
542
613
  const t = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
543
614
  const who = m.from ?? "?";
615
+ const color = agentColor(who);
616
+ const gutter = color("▎");
544
617
  const tag = kind === "DM" ? A.bold(A.cyan("DM")) : A.dim("room");
545
- const meta = `${A.dim(t)} ${tag} ${agentColor(who)(who)}`;
546
- // Multi-line bodies: first line on the meta row, subsequent lines indented
547
- // to the body column for readability.
548
- const body = (m.text ?? "").split("\n");
549
- const indent = " ";
618
+ const nick = A.bold(color(who));
619
+ const meta = `${A.dim(t)} ${tag} ${nick}`;
620
+ const body = (m.text ?? "").split("\n").map(formatBody);
621
+ const indent = " ";
550
622
  const first = body[0] ?? "";
551
- const rest = body.slice(1).map((l) => indent + A.dim("│ ") + l);
623
+ const rest = body.slice(1).map((l) => `${gutter} ${indent}${A.dim("│ ")}${l}`);
552
624
  const prefix = opts.history ? A.dim(" ") : "";
553
- say(`${prefix}${meta} ${first}`);
625
+ say(`${prefix}${gutter} ${meta} ${first}`);
554
626
  for (const line of rest) say(`${prefix}${line}`);
555
627
  }
556
628
 
629
+ // Lightweight inline-only "chat markdown" formatter — no dep. Handles bold,
630
+ // italic, inline code, links, and @mentions. Order matters: pull out inline
631
+ // code spans first so we don't touch their contents, then run the rest.
632
+ function formatBody(text) {
633
+ return text.split(/(`[^`\n]+`)/).map((part) => {
634
+ if (part.startsWith("`") && part.endsWith("`") && part.length >= 2) {
635
+ return A.dim("`") + A.cyan(part.slice(1, -1)) + A.dim("`");
636
+ }
637
+ let s = part;
638
+ // @mentions first — colored in the mentioned agent's hash color, bold if
639
+ // it's the current user (so you can spot pings at a glance).
640
+ s = s.replace(/@([A-Za-z0-9._-]+)/g, (_, name) => {
641
+ const colored = agentColor(name)(`@${name}`);
642
+ return name === ID ? A.bold(colored) : colored;
643
+ });
644
+ // **bold**
645
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, (_, t) => A.bold(t));
646
+ // *italic* and _italic_ (avoid matching inside **bold** by requiring
647
+ // non-asterisk neighbors)
648
+ s = s.replace(/(?<![*\w])\*([^*\n]+)\*(?![*\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
649
+ s = s.replace(/(?<![_\w])_([^_\n]+)_(?![_\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
650
+ // [text](url) — show text underlined with a dim trailing (url)
651
+ s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, (_, t, u) =>
652
+ `\x1b[4m${t}\x1b[0m${A.dim(` (${u})`)}`,
653
+ );
654
+ // Bare URLs — underline only the URL itself
655
+ s = s.replace(/\bhttps?:\/\/[^\s)]+/g, (u) => `\x1b[4m${u}\x1b[0m`);
656
+ return s;
657
+ }).join("");
658
+ }
659
+
557
660
  async function printAgents() {
558
661
  const reg = readJsonSafe(AGENTS_FILE, {});
559
662
  const now = Date.now();