agent-coord-mcp 0.4.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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": {
@@ -74,17 +74,15 @@ const A = {
74
74
  brightCyan: (s) => `\x1b[96m${s}\x1b[0m`,
75
75
  };
76
76
 
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.
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
- function agentColor(id) {
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 and DM targets. On multi-match with no
128
- // common-prefix advancement, surface the options on the first Tab via
129
- // say() — default readline UX hides them until a second Tab, which most
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
- if (line.startsWith("/dm ")) {
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 <= line.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, line];
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
- await drainAndPrint();
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" }));