agent-coord-mcp 0.4.7 → 0.4.8

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.7",
3
+ "version": "0.4.8",
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": {
@@ -44,6 +44,33 @@ const args = parseArgs(process.argv.slice(2));
44
44
  const ID = args.id ?? process.env.USER ?? "human";
45
45
  const ROOT = args.dir ?? process.env.AGENT_COORD_DIR ?? path.join(homedir(), "agent-coord");
46
46
 
47
+ // Message-rendering state + helpers. Declared up here (above the top-level
48
+ // printRecent() call) so they're initialized before first use — const/let
49
+ // don't hoist the way function declarations do.
50
+
51
+ // Consecutive messages from the same sender within this window are visually
52
+ // grouped: the second one drops its header/blank line and just continues the
53
+ // gutter, Slack-style.
54
+ const GROUP_WINDOW = 2 * 60 * 1000;
55
+ let lastBlock = { who: null, ts: 0, kind: null };
56
+
57
+ // Matches "@<this agent>" not followed by a name char, so we can flag messages
58
+ // that ping the current user. ID may contain regex metachars — escape it.
59
+ const SELF_MENTION_RE = new RegExp(
60
+ "@" + ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![A-Za-z0-9._-])",
61
+ );
62
+ const mentionsSelf = (text) => SELF_MENTION_RE.test(text ?? "");
63
+
64
+ // Recency at a glance: "now" / "5m" for fresh messages, falling back to a wall
65
+ // clock for anything over an hour (a stale "63m" reads worse than "08:34").
66
+ function relTime(ts) {
67
+ const mins = Math.floor((Date.now() - ts) / 60000);
68
+ if (mins < 1) return "now";
69
+ if (mins < 60) return `${mins}m`;
70
+ const d = new Date(ts);
71
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
72
+ }
73
+
47
74
  const INBOX_DIR = path.join(ROOT, "inbox");
48
75
  const CURSOR_DIR = path.join(ROOT, "cursors");
49
76
  const TRANSPORT_DIR = path.join(ROOT, "transports");
@@ -136,14 +163,12 @@ function completer(line) {
136
163
  const mentionMatch = line.match(/@([A-Za-z0-9._-]*)$/);
137
164
  if (mentionMatch) {
138
165
  const partial = mentionMatch[1];
139
- const reg = readJsonSafe(AGENTS_FILE, {});
140
- const ids = Object.keys(reg).filter((id) => id.startsWith(partial));
166
+ const ids = onlineAgentIds().filter((id) => id !== ID && id.startsWith(partial));
141
167
  hits = ids.map((id) => `@${id} `);
142
168
  substr = mentionMatch[0]; // tell readline to replace just the @partial part
143
169
  } else if (line.startsWith("/dm ")) {
144
170
  const partial = line.slice(4);
145
- const reg = readJsonSafe(AGENTS_FILE, {});
146
- const ids = Object.keys(reg).filter((id) => id !== ID && id.startsWith(partial));
171
+ const ids = onlineAgentIds().filter((id) => id !== ID && id.startsWith(partial));
147
172
  hits = ids.map((id) => `/dm ${id} `);
148
173
  } else if (line.startsWith("/")) {
149
174
  hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
@@ -171,6 +196,17 @@ const rl = readline.createInterface({
171
196
  completer,
172
197
  });
173
198
 
199
+ // Auto-offer the logged-in agents the instant "@" is typed (editor-style),
200
+ // so you don't have to press Tab to discover who's reachable. We only observe
201
+ // keypresses — readline still owns input. setImmediate lets readline insert
202
+ // the "@" into its line buffer before we inspect it.
203
+ if (process.stdin.isTTY) {
204
+ readline.emitKeypressEvents(process.stdin);
205
+ process.stdin.on("keypress", (str) => {
206
+ if (str === "@") setImmediate(showMentionPicker);
207
+ });
208
+ }
209
+
174
210
  // Banner — printed once on launch. Keep it tight; this is a CLI, not a poster.
175
211
  printBanner();
176
212
  // Show recent context (last 3 messages from inbox + room) then fast-forward
@@ -661,29 +697,6 @@ async function drainAndPrint() {
661
697
  if (changed) writeJsonAtomic(CURSOR_FILE, cursor);
662
698
  }
663
699
 
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
-
687
700
  function printMsg(kind, m, opts = {}) {
688
701
  const who = m.from ?? "?";
689
702
  const color = agentColor(who);
@@ -836,6 +849,38 @@ function pidAlive(pid) {
836
849
  catch (e) { return e?.code === "EPERM"; }
837
850
  }
838
851
 
852
+ // Agents considered "logged in": a live transport process, or a heartbeat
853
+ // within the stale window. Shared by the @mention picker and completer so we
854
+ // only ever offer reachable agents.
855
+ function onlineAgentIds() {
856
+ const reg = readJsonSafe(AGENTS_FILE, {});
857
+ const now = Date.now();
858
+ const STALE = 5 * 60 * 1000;
859
+ return Object.keys(reg)
860
+ .filter((id) => {
861
+ const a = reg[id];
862
+ const marker = readJsonSafe(path.join(TRANSPORT_DIR, `${sanitize(id)}.json`), null);
863
+ const live = marker && marker.pid && pidAlive(marker.pid);
864
+ return live || now - (a?.lastHeartbeat ?? 0) < STALE;
865
+ })
866
+ .sort();
867
+ }
868
+
869
+ // Pop the list of logged-in agents the moment "@" starts a mention token, so
870
+ // you can see who's reachable without hunting through /list. The list is
871
+ // dim/cosmetic and re-renders above the preserved input line.
872
+ function showMentionPicker() {
873
+ if (typeof rl === "undefined") return;
874
+ const before = (rl.line ?? "").slice(0, rl.cursor ?? (rl.line ?? "").length);
875
+ // Only when the just-typed "@" opens a fresh token (start of line or after
876
+ // whitespace) — avoids firing inside emails or mid-word.
877
+ if (!/(^|\s)@$/.test(before)) return;
878
+ const ids = onlineAgentIds().filter((id) => id !== ID);
879
+ if (!ids.length) return;
880
+ const list = ids.map((id) => A.green("●") + agentColor(id)(`@${id}`)).join(" ");
881
+ say(A.dim(" ┄ ") + list + A.dim(" · Tab to complete"));
882
+ }
883
+
839
884
  function readJsonl(file) {
840
885
  if (!existsSync(file)) return [];
841
886
  return readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => {