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 +0 -0
- package/package.json +1 -1
- package/scripts/coord-chat.mjs +106 -15
package/dist/server.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -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
|
-
|
|
679
|
-
|
|
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();
|