agent-coord-mcp 0.4.3 → 0.4.6
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 +110 -21
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -74,17 +74,15 @@ const A = {
|
|
|
74
74
|
brightCyan: (s) => `\x1b[96m${s}\x1b[0m`,
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
// Stable per-agent color
|
|
78
|
-
//
|
|
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.
|
|
79
81
|
const AGENT_COLORS = [
|
|
80
82
|
A.green, A.yellow, A.blue, A.magenta, A.cyan,
|
|
81
83
|
A.brightGreen, A.brightYellow, A.brightBlue, A.brightMagenta, A.brightCyan,
|
|
82
84
|
];
|
|
83
|
-
|
|
84
|
-
let h = 0;
|
|
85
|
-
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
|
86
|
-
return AGENT_COLORS[h % AGENT_COLORS.length];
|
|
87
|
-
}
|
|
85
|
+
// Will be initialized after ROOT is set, just below.
|
|
88
86
|
|
|
89
87
|
// ---------- register and start UI ----------
|
|
90
88
|
|
|
@@ -122,14 +120,27 @@ const SLASH_COMMANDS = [
|
|
|
122
120
|
];
|
|
123
121
|
|
|
124
122
|
const STATUS_FILE_PATH = path.join(ROOT, "status.jsonl");
|
|
123
|
+
const COLOR_MAP_FILE = path.join(ROOT, "chat-colors.json");
|
|
125
124
|
|
|
126
125
|
function completer(line) {
|
|
127
|
-
// Tab-complete slash commands
|
|
128
|
-
// common-prefix advancement, surface the options
|
|
129
|
-
// say() — default readline UX hides them until a
|
|
130
|
-
// 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."
|
|
131
130
|
let hits = [];
|
|
132
|
-
|
|
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 ")) {
|
|
133
144
|
const partial = line.slice(4);
|
|
134
145
|
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
135
146
|
const ids = Object.keys(reg).filter((id) => id !== ID && id.startsWith(partial));
|
|
@@ -137,11 +148,11 @@ function completer(line) {
|
|
|
137
148
|
} else if (line.startsWith("/")) {
|
|
138
149
|
hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
139
150
|
}
|
|
140
|
-
if (hits.length > 1 && commonPrefix(hits).length <=
|
|
151
|
+
if (hits.length > 1 && commonPrefix(hits).length <= substr.length) {
|
|
141
152
|
const display = hits.map((h) => h.trim()).join(" ");
|
|
142
153
|
say(A.dim(" ┄ " + display));
|
|
143
154
|
}
|
|
144
|
-
return [hits,
|
|
155
|
+
return [hits, substr];
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
function commonPrefix(strs) {
|
|
@@ -162,7 +173,10 @@ const rl = readline.createInterface({
|
|
|
162
173
|
|
|
163
174
|
// Banner — printed once on launch. Keep it tight; this is a CLI, not a poster.
|
|
164
175
|
printBanner();
|
|
165
|
-
|
|
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);
|
|
166
180
|
|
|
167
181
|
// Lay down the first separator. From this point, async incoming messages
|
|
168
182
|
// (via the watcher → drainAndPrint → say) know they can use the cursor
|
|
@@ -280,6 +294,34 @@ function sanitize(s) {
|
|
|
280
294
|
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
281
295
|
}
|
|
282
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
|
+
|
|
283
325
|
function say(line) {
|
|
284
326
|
if (lastLineWasSep) {
|
|
285
327
|
// Async path: a separator we own sits directly above the prompt; we own
|
|
@@ -373,6 +415,17 @@ async function printWhoami() {
|
|
|
373
415
|
say(` ${A.cyan("registered")} ${a ? A.green("yes") : A.red("no")}`);
|
|
374
416
|
}
|
|
375
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
|
+
|
|
376
429
|
async function printRecent(n) {
|
|
377
430
|
const inbox = readJsonl(INBOX_FILE).slice(-n).map((m) => ({ ...m, _kind: "DM" }));
|
|
378
431
|
const room = readJsonl(ROOM_FILE).slice(-n).map((m) => ({ ...m, _kind: "room" }));
|
|
@@ -617,13 +670,49 @@ function printMsg(kind, m, opts = {}) {
|
|
|
617
670
|
const tag = kind === "DM" ? A.bold(A.cyan("DM")) : A.dim("room");
|
|
618
671
|
const nick = A.bold(color(who));
|
|
619
672
|
const meta = `${A.dim(t)} ${tag} ${nick}`;
|
|
620
|
-
const body = (m.text ?? "").split("\n").map(formatBody);
|
|
621
|
-
const indent = " ";
|
|
622
|
-
const first = body[0] ?? "";
|
|
623
|
-
const rest = body.slice(1).map((l) => `${gutter} ${indent}${A.dim("│ ")}${l}`);
|
|
624
673
|
const prefix = opts.history ? A.dim(" ") : "";
|
|
625
|
-
|
|
626
|
-
|
|
674
|
+
|
|
675
|
+
// Visible widths: prefix + "▎ " + meta + " " on the first line;
|
|
676
|
+
// prefix + "▎ " on continuation lines. We wrap manually so the
|
|
677
|
+
// colored gutter prepends every wrapped line — terminal auto-wrap
|
|
678
|
+
// would lose it.
|
|
679
|
+
const prefixW = visibleLength(prefix);
|
|
680
|
+
const firstChrome = prefixW + visibleLength(`▎ ${meta} `);
|
|
681
|
+
const restChrome = prefixW + visibleLength(`▎ `);
|
|
682
|
+
const firstWidth = Math.max(20, COLS - firstChrome);
|
|
683
|
+
const restWidth = Math.max(20, COLS - restChrome);
|
|
684
|
+
|
|
685
|
+
const text = (m.text ?? "").split("\n").map(formatBody).join("\n");
|
|
686
|
+
const lines = wrapText(text, firstWidth, restWidth);
|
|
687
|
+
say(`${prefix}${gutter} ${meta} ${lines[0] ?? ""}`);
|
|
688
|
+
for (const line of lines.slice(1)) say(`${prefix}${gutter} ${line}`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function visibleLength(s) {
|
|
692
|
+
// Strip ANSI SGR sequences so we measure on-screen width, not raw bytes.
|
|
693
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function wrapText(text, firstWidth, restWidth) {
|
|
697
|
+
if (firstWidth <= 0 || restWidth <= 0) return [text];
|
|
698
|
+
const out = [];
|
|
699
|
+
for (const para of text.split("\n")) {
|
|
700
|
+
const words = para.split(" ");
|
|
701
|
+
let line = "";
|
|
702
|
+
let currentWidth = out.length === 0 ? firstWidth : restWidth;
|
|
703
|
+
for (const word of words) {
|
|
704
|
+
const proposed = line ? line + " " + word : word;
|
|
705
|
+
if (visibleLength(proposed) > currentWidth && line) {
|
|
706
|
+
out.push(line);
|
|
707
|
+
line = word;
|
|
708
|
+
currentWidth = restWidth;
|
|
709
|
+
} else {
|
|
710
|
+
line = proposed;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (line || words.length === 0) out.push(line);
|
|
714
|
+
}
|
|
715
|
+
return out;
|
|
627
716
|
}
|
|
628
717
|
|
|
629
718
|
// Lightweight inline-only "chat markdown" formatter — no dep. Handles bold,
|