agent-coord-mcp 0.4.1 → 0.4.5
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 +1 -1
- package/scripts/coord-chat.mjs +116 -23
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -67,16 +67,22 @@ 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
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
// Stable per-agent color via a persistent registry shared by all coord-chat
|
|
78
|
+
// sessions. First time we see an agentId we pick the next unused palette
|
|
79
|
+
// slot — guarantees no collisions until the palette is exhausted. After that
|
|
80
|
+
// we fall back to hashing so behavior stays deterministic.
|
|
81
|
+
const AGENT_COLORS = [
|
|
82
|
+
A.green, A.yellow, A.blue, A.magenta, A.cyan,
|
|
83
|
+
A.brightGreen, A.brightYellow, A.brightBlue, A.brightMagenta, A.brightCyan,
|
|
84
|
+
];
|
|
85
|
+
// Will be initialized after ROOT is set, just below.
|
|
80
86
|
|
|
81
87
|
// ---------- register and start UI ----------
|
|
82
88
|
|
|
@@ -114,14 +120,27 @@ const SLASH_COMMANDS = [
|
|
|
114
120
|
];
|
|
115
121
|
|
|
116
122
|
const STATUS_FILE_PATH = path.join(ROOT, "status.jsonl");
|
|
123
|
+
const COLOR_MAP_FILE = path.join(ROOT, "chat-colors.json");
|
|
117
124
|
|
|
118
125
|
function completer(line) {
|
|
119
|
-
// Tab-complete slash commands
|
|
120
|
-
// common-prefix advancement, surface the options
|
|
121
|
-
// say() — default readline UX hides them until a
|
|
122
|
-
// users assume means "nothing happened."
|
|
126
|
+
// Tab-complete slash commands, DM targets, and @mentions mid-message.
|
|
127
|
+
// On multi-match with no common-prefix advancement, surface the options
|
|
128
|
+
// on the first Tab via say() — default readline UX hides them until a
|
|
129
|
+
// second Tab, which most users assume means "nothing happened."
|
|
123
130
|
let hits = [];
|
|
124
|
-
|
|
131
|
+
let substr = line;
|
|
132
|
+
|
|
133
|
+
// @mention completion takes priority — checked first because it can
|
|
134
|
+
// appear inside a slash command argument (e.g. `/dm bob hey @ali`) or
|
|
135
|
+
// in a plain room message.
|
|
136
|
+
const mentionMatch = line.match(/@([A-Za-z0-9._-]*)$/);
|
|
137
|
+
if (mentionMatch) {
|
|
138
|
+
const partial = mentionMatch[1];
|
|
139
|
+
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
140
|
+
const ids = Object.keys(reg).filter((id) => id.startsWith(partial));
|
|
141
|
+
hits = ids.map((id) => `@${id} `);
|
|
142
|
+
substr = mentionMatch[0]; // tell readline to replace just the @partial part
|
|
143
|
+
} else if (line.startsWith("/dm ")) {
|
|
125
144
|
const partial = line.slice(4);
|
|
126
145
|
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
127
146
|
const ids = Object.keys(reg).filter((id) => id !== ID && id.startsWith(partial));
|
|
@@ -129,11 +148,11 @@ function completer(line) {
|
|
|
129
148
|
} else if (line.startsWith("/")) {
|
|
130
149
|
hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
131
150
|
}
|
|
132
|
-
if (hits.length > 1 && commonPrefix(hits).length <=
|
|
151
|
+
if (hits.length > 1 && commonPrefix(hits).length <= substr.length) {
|
|
133
152
|
const display = hits.map((h) => h.trim()).join(" ");
|
|
134
153
|
say(A.dim(" ┄ " + display));
|
|
135
154
|
}
|
|
136
|
-
return [hits,
|
|
155
|
+
return [hits, substr];
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
function commonPrefix(strs) {
|
|
@@ -154,7 +173,10 @@ const rl = readline.createInterface({
|
|
|
154
173
|
|
|
155
174
|
// Banner — printed once on launch. Keep it tight; this is a CLI, not a poster.
|
|
156
175
|
printBanner();
|
|
157
|
-
|
|
176
|
+
// Show recent context (last 3 messages from inbox + room) then fast-forward
|
|
177
|
+
// the cursor so the same entries don't show up again via the watcher path.
|
|
178
|
+
fastForwardCursors();
|
|
179
|
+
await printRecent(3);
|
|
158
180
|
|
|
159
181
|
// Lay down the first separator. From this point, async incoming messages
|
|
160
182
|
// (via the watcher → drainAndPrint → say) know they can use the cursor
|
|
@@ -272,6 +294,34 @@ function sanitize(s) {
|
|
|
272
294
|
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
273
295
|
}
|
|
274
296
|
|
|
297
|
+
function agentColor(id) {
|
|
298
|
+
const map = readJsonSafe(COLOR_MAP_FILE, {});
|
|
299
|
+
const existing = map[id];
|
|
300
|
+
if (typeof existing === "number" && existing >= 0 && existing < AGENT_COLORS.length) {
|
|
301
|
+
return AGENT_COLORS[existing];
|
|
302
|
+
}
|
|
303
|
+
// First sighting — pick the first unused palette slot.
|
|
304
|
+
const used = new Set(Object.values(map).filter((v) => typeof v === "number"));
|
|
305
|
+
let idx = -1;
|
|
306
|
+
for (let i = 0; i < AGENT_COLORS.length; i++) {
|
|
307
|
+
if (!used.has(i)) { idx = i; break; }
|
|
308
|
+
}
|
|
309
|
+
if (idx === -1) {
|
|
310
|
+
// Palette exhausted — deterministic hash fallback. No persist (don't
|
|
311
|
+
// pollute the map with hash assignments that could be wrong).
|
|
312
|
+
let h = 0;
|
|
313
|
+
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
|
314
|
+
return AGENT_COLORS[h % AGENT_COLORS.length];
|
|
315
|
+
}
|
|
316
|
+
// Persist. Re-read from disk first to merge any concurrent assignments
|
|
317
|
+
// by other coord-chat processes; last-writer-wins for a single agentId
|
|
318
|
+
// is fine since colors are cosmetic.
|
|
319
|
+
const onDisk = readJsonSafe(COLOR_MAP_FILE, {});
|
|
320
|
+
onDisk[id] = idx;
|
|
321
|
+
try { writeJsonAtomic(COLOR_MAP_FILE, onDisk); } catch { /* best effort */ }
|
|
322
|
+
return AGENT_COLORS[idx];
|
|
323
|
+
}
|
|
324
|
+
|
|
275
325
|
function say(line) {
|
|
276
326
|
if (lastLineWasSep) {
|
|
277
327
|
// Async path: a separator we own sits directly above the prompt; we own
|
|
@@ -365,6 +415,17 @@ async function printWhoami() {
|
|
|
365
415
|
say(` ${A.cyan("registered")} ${a ? A.green("yes") : A.red("no")}`);
|
|
366
416
|
}
|
|
367
417
|
|
|
418
|
+
function fastForwardCursors() {
|
|
419
|
+
// Move our cursor offsets to end-of-file so anything that existed before
|
|
420
|
+
// launch is treated as already-seen. printRecent(N) then shows the last N
|
|
421
|
+
// as historical context, and the watcher path only fires for genuinely
|
|
422
|
+
// new messages going forward.
|
|
423
|
+
const cur = readJsonSafe(CURSOR_FILE, {});
|
|
424
|
+
cur.inboxOffset = readJsonl(INBOX_FILE).length;
|
|
425
|
+
cur.roomOffset = readJsonl(ROOM_FILE).length;
|
|
426
|
+
writeJsonAtomic(CURSOR_FILE, cur);
|
|
427
|
+
}
|
|
428
|
+
|
|
368
429
|
async function printRecent(n) {
|
|
369
430
|
const inbox = readJsonl(INBOX_FILE).slice(-n).map((m) => ({ ...m, _kind: "DM" }));
|
|
370
431
|
const room = readJsonl(ROOM_FILE).slice(-n).map((m) => ({ ...m, _kind: "room" }));
|
|
@@ -604,19 +665,51 @@ function printMsg(kind, m, opts = {}) {
|
|
|
604
665
|
const d = new Date(m.ts);
|
|
605
666
|
const t = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
606
667
|
const who = m.from ?? "?";
|
|
668
|
+
const color = agentColor(who);
|
|
669
|
+
const gutter = color("▎");
|
|
607
670
|
const tag = kind === "DM" ? A.bold(A.cyan("DM")) : A.dim("room");
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
const indent = " ";
|
|
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 = " ";
|
|
613
675
|
const first = body[0] ?? "";
|
|
614
|
-
const rest = body.slice(1).map((l) => indent
|
|
676
|
+
const rest = body.slice(1).map((l) => `${gutter} ${indent}${A.dim("│ ")}${l}`);
|
|
615
677
|
const prefix = opts.history ? A.dim(" ") : "";
|
|
616
|
-
say(`${prefix}${meta} ${first}`);
|
|
678
|
+
say(`${prefix}${gutter} ${meta} ${first}`);
|
|
617
679
|
for (const line of rest) say(`${prefix}${line}`);
|
|
618
680
|
}
|
|
619
681
|
|
|
682
|
+
// Lightweight inline-only "chat markdown" formatter — no dep. Handles bold,
|
|
683
|
+
// italic, inline code, links, and @mentions. Order matters: pull out inline
|
|
684
|
+
// code spans first so we don't touch their contents, then run the rest.
|
|
685
|
+
function formatBody(text) {
|
|
686
|
+
return text.split(/(`[^`\n]+`)/).map((part) => {
|
|
687
|
+
if (part.startsWith("`") && part.endsWith("`") && part.length >= 2) {
|
|
688
|
+
return A.dim("`") + A.cyan(part.slice(1, -1)) + A.dim("`");
|
|
689
|
+
}
|
|
690
|
+
let s = part;
|
|
691
|
+
// @mentions first — colored in the mentioned agent's hash color, bold if
|
|
692
|
+
// it's the current user (so you can spot pings at a glance).
|
|
693
|
+
s = s.replace(/@([A-Za-z0-9._-]+)/g, (_, name) => {
|
|
694
|
+
const colored = agentColor(name)(`@${name}`);
|
|
695
|
+
return name === ID ? A.bold(colored) : colored;
|
|
696
|
+
});
|
|
697
|
+
// **bold**
|
|
698
|
+
s = s.replace(/\*\*([^*\n]+)\*\*/g, (_, t) => A.bold(t));
|
|
699
|
+
// *italic* and _italic_ (avoid matching inside **bold** by requiring
|
|
700
|
+
// non-asterisk neighbors)
|
|
701
|
+
s = s.replace(/(?<![*\w])\*([^*\n]+)\*(?![*\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
|
|
702
|
+
s = s.replace(/(?<![_\w])_([^_\n]+)_(?![_\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
|
|
703
|
+
// [text](url) — show text underlined with a dim trailing (url)
|
|
704
|
+
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, (_, t, u) =>
|
|
705
|
+
`\x1b[4m${t}\x1b[0m${A.dim(` (${u})`)}`,
|
|
706
|
+
);
|
|
707
|
+
// Bare URLs — underline only the URL itself
|
|
708
|
+
s = s.replace(/\bhttps?:\/\/[^\s)]+/g, (u) => `\x1b[4m${u}\x1b[0m`);
|
|
709
|
+
return s;
|
|
710
|
+
}).join("");
|
|
711
|
+
}
|
|
712
|
+
|
|
620
713
|
async function printAgents() {
|
|
621
714
|
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
622
715
|
const now = Date.now();
|