agent-coord-mcp 0.4.5 → 0.4.7

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/dist/server.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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": {
@@ -661,22 +661,98 @@ async function drainAndPrint() {
661
661
  if (changed) writeJsonAtomic(CURSOR_FILE, cursor);
662
662
  }
663
663
 
664
+ // Consecutive messages from the same sender within this window are visually
665
+ // grouped: the second one drops its header/blank line and just continues the
666
+ // gutter, Slack-style.
667
+ const GROUP_WINDOW = 2 * 60 * 1000;
668
+ let lastBlock = { who: null, ts: 0, kind: null };
669
+
670
+ // Matches "@<this agent>" not followed by a name char, so we can flag messages
671
+ // that ping the current user. ID may contain regex metachars — escape it.
672
+ const SELF_MENTION_RE = new RegExp(
673
+ "@" + ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![A-Za-z0-9._-])",
674
+ );
675
+ const mentionsSelf = (text) => SELF_MENTION_RE.test(text ?? "");
676
+
677
+ // Recency at a glance: "now" / "5m" for fresh messages, falling back to a wall
678
+ // clock for anything over an hour (a stale "63m" reads worse than "08:34").
679
+ function relTime(ts) {
680
+ const mins = Math.floor((Date.now() - ts) / 60000);
681
+ if (mins < 1) return "now";
682
+ if (mins < 60) return `${mins}m`;
683
+ const d = new Date(ts);
684
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
685
+ }
686
+
664
687
  function printMsg(kind, m, opts = {}) {
665
- const d = new Date(m.ts);
666
- const t = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
667
688
  const who = m.from ?? "?";
668
689
  const color = agentColor(who);
669
690
  const gutter = color("▎");
670
- const tag = kind === "DM" ? A.bold(A.cyan("DM")) : A.dim("room");
671
- const nick = A.bold(color(who));
672
- const meta = `${A.dim(t)} ${tag} ${nick}`;
673
- const body = (m.text ?? "").split("\n").map(formatBody);
674
- const indent = " ";
675
- const first = body[0] ?? "";
676
- const rest = body.slice(1).map((l) => `${gutter} ${indent}${A.dim("│ ")}${l}`);
677
691
  const prefix = opts.history ? A.dim(" ") : "";
678
- say(`${prefix}${gutter} ${meta} ${first}`);
679
- for (const line of rest) say(`${prefix}${line}`);
692
+
693
+ // Body wraps manually under a continuous gutter — terminal auto-wrap would
694
+ // lose the colored gutter on continuation lines.
695
+ const prefixW = visibleLength(prefix);
696
+ const bodyWidth = Math.max(20, COLS - prefixW - visibleLength(`▎ `));
697
+ const text = (m.text ?? "").split("\n").map(formatBody).join("\n");
698
+ const lines = wrapBody(text, bodyWidth);
699
+
700
+ // Group onto the previous block when it's the same live sender within the
701
+ // window — skip the blank line + header, just keep the gutter going.
702
+ const grouped = !opts.history
703
+ && lastBlock.who === who && lastBlock.kind === kind
704
+ && (m.ts - lastBlock.ts) < GROUP_WINDOW;
705
+
706
+ if (!grouped) {
707
+ // Header on its own line so the sender is a scannable anchor and the body
708
+ // always starts at a fixed column. "room" is the default and stays
709
+ // implied; only DMs get a badge. A ping to the current user brightens the
710
+ // gutter and adds a ► marker so it pops out of the firehose.
711
+ const pinged = mentionsSelf(m.text);
712
+ const badge = kind === "DM" ? A.bold(A.cyan("DM ")) : "";
713
+ const marker = pinged ? A.bold(A.yellow("► ")) : "";
714
+ const headGutter = pinged ? A.bold(color("▌")) : gutter;
715
+ const header = `${marker}${badge}${A.bold(color(who))} ${A.dim(`· ${relTime(m.ts)}`)}`;
716
+ say("");
717
+ say(`${prefix}${headGutter} ${header}`);
718
+ }
719
+ for (const line of lines) say(`${prefix}${gutter} ${line}`);
720
+
721
+ if (!opts.history) lastBlock = { who, ts: m.ts, kind };
722
+ }
723
+
724
+ function visibleLength(s) {
725
+ // Strip ANSI SGR sequences so we measure on-screen width, not raw bytes.
726
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
727
+ }
728
+
729
+ // Wrap one message's text, preserving list/indent structure: a leading bullet
730
+ // ("- ", "* ", "1. ", "2) ") or whitespace indent is detected so wrapped
731
+ // continuation lines hang-indent under the text rather than re-flowing as flat
732
+ // prose.
733
+ function wrapBody(text, width) {
734
+ if (width <= 0) return [text];
735
+ const out = [];
736
+ for (const raw of text.split("\n")) {
737
+ const mk = raw.match(/^(\s*(?:[-*•]\s+|\d+[.)]\s+)?)([\s\S]*)$/);
738
+ const lead = mk ? mk[1] : "";
739
+ const body = mk ? mk[2] : raw;
740
+ const indent = " ".repeat(visibleLength(lead));
741
+ const words = body.length ? body.split(/\s+/) : [];
742
+ if (!words.length) { out.push(lead.trimEnd()); continue; }
743
+ let line = lead + words[0];
744
+ for (let i = 1; i < words.length; i++) {
745
+ const proposed = line + " " + words[i];
746
+ if (visibleLength(proposed) > width) {
747
+ out.push(line);
748
+ line = indent + words[i];
749
+ } else {
750
+ line = proposed;
751
+ }
752
+ }
753
+ out.push(line);
754
+ }
755
+ return out;
680
756
  }
681
757
 
682
758
  // Lightweight inline-only "chat markdown" formatter — no dep. Handles bold,
@@ -700,16 +776,31 @@ function formatBody(text) {
700
776
  // non-asterisk neighbors)
701
777
  s = s.replace(/(?<![*\w])\*([^*\n]+)\*(?![*\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
702
778
  s = s.replace(/(?<![_\w])_([^_\n]+)_(?![_\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
703
- // [text](url) — show text underlined with a dim trailing (url)
779
+ // [text](url) — show text underlined with a dim, shortened trailing (url)
704
780
  s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, (_, t, u) =>
705
- `\x1b[4m${t}\x1b[0m${A.dim(` (${u})`)}`,
781
+ `\x1b[4m${t}\x1b[0m${A.dim(` (${shortenUrl(u)})`)}`,
706
782
  );
707
- // Bare URLs — underline only the URL itself
708
- s = s.replace(/\bhttps?:\/\/[^\s)]+/g, (u) => `\x1b[4m${u}\x1b[0m`);
783
+ // Bare URLs — underline only the URL itself, shortened if long
784
+ s = s.replace(/\bhttps?:\/\/[^\s)]+/g, (u) => `\x1b[4m${shortenUrl(u)}\x1b[0m`);
709
785
  return s;
710
786
  }).join("");
711
787
  }
712
788
 
789
+ // Long URLs eat a whole wrapped line. Collapse to "host/…/last-segment" so the
790
+ // link stays recognizable without dominating the message. Short URLs are left
791
+ // intact (and remain copy-pasteable).
792
+ function shortenUrl(u) {
793
+ if (u.length <= 48) return u;
794
+ try {
795
+ const { host, pathname } = new URL(u);
796
+ const tail = pathname.split("/").filter(Boolean).pop() ?? "";
797
+ const short = tail ? `${host}/…/${tail}` : host;
798
+ return short.length < u.length ? short : u.slice(0, 45) + "…";
799
+ } catch {
800
+ return u.slice(0, 45) + "…";
801
+ }
802
+ }
803
+
713
804
  async function printAgents() {
714
805
  const reg = readJsonSafe(AGENTS_FILE, {});
715
806
  const now = Date.now();