agent-coord-mcp 0.4.9 → 0.5.0

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.
@@ -59,6 +59,10 @@ if (!TMUX_TARGET) die("AGENT_COORD_TMUX_TARGET is required");
59
59
 
60
60
  const ROOT = process.env.AGENT_COORD_DIR || path.join(homedir(), "agent-coord");
61
61
  const INCLUDE_ROOM = process.env.AGENT_COORD_INCLUDE_ROOM === "1";
62
+ // Default channel + watch registry — declared up here so the hoisted channel
63
+ // helpers, called from the top-level checkOnce(), don't trip the const TDZ.
64
+ const DEFAULT_ROOM = "general";
65
+ const watchedRooms = new Set();
62
66
  const ALLOWLIST = (process.env.AGENT_COORD_ALLOWLIST || "")
63
67
  .split(",")
64
68
  .map((s) => s.trim())
@@ -68,7 +72,6 @@ const POLL_MS = parseInt(process.env.AGENT_COORD_POLL_MS || "1000", 10);
68
72
 
69
73
  const SAFE_ID = AGENT_ID.replace(/[^a-zA-Z0-9._-]/g, "_");
70
74
  const INBOX_FILE = path.join(ROOT, "inbox", `${SAFE_ID}.jsonl`);
71
- const ROOM_FILE = path.join(ROOT, "room.jsonl");
72
75
  const CURSOR_FILE = path.join(ROOT, "cursors", `${SAFE_ID}.json`);
73
76
  const TRANSPORT_FILE = path.join(ROOT, "transports", `${SAFE_ID}.json`);
74
77
  const BUFFER_NAME = `coord-${SAFE_ID}`;
@@ -135,11 +138,33 @@ function drainSource(label, file, cursorKey, cur) {
135
138
  return true;
136
139
  }
137
140
 
141
+ // Drain one channel against its per-channel offset (general → roomOffset,
142
+ // others → roomOffsets[chan]); tag injected lines with the channel name.
143
+ function drainRoomChannel(chan, cur) {
144
+ const c = normalizeRoom(chan);
145
+ const all = readJsonl(roomFile(c));
146
+ const off = getRoomOffset(cur, c);
147
+ const fresh = all.slice(off);
148
+ if (fresh.length === 0) return false;
149
+ for (const m of fresh) {
150
+ if (shouldInject(m)) pending.push({ kind: `room #${c}`, ...m });
151
+ }
152
+ setRoomOffset(cur, c, off + fresh.length);
153
+ return true;
154
+ }
155
+
138
156
  function checkOnce() {
139
157
  const cur = readCursor();
140
158
  let changed = false;
141
159
  if (drainSource("DM", INBOX_FILE, "inboxOffset", cur)) changed = true;
142
- if (INCLUDE_ROOM && drainSource("ROOM", ROOM_FILE, "roomOffset", cur)) changed = true;
160
+ if (INCLUDE_ROOM) {
161
+ // Tail every channel the agent has joined. checkOnce runs on each poll, so
162
+ // channels joined after startup are picked up automatically.
163
+ for (const chan of joinedRooms()) {
164
+ if (drainRoomChannel(chan, cur)) changed = true;
165
+ watchRoom(chan);
166
+ }
167
+ }
143
168
  if (changed) writeCursor(cur);
144
169
  if (pending.length > 0) scheduleFlush();
145
170
  }
@@ -255,11 +280,7 @@ try {
255
280
  // file may not exist yet; polling covers it
256
281
  }
257
282
  if (INCLUDE_ROOM) {
258
- try {
259
- if (existsSync(ROOM_FILE)) watch(ROOM_FILE, () => checkOnce());
260
- } catch {
261
- // ignore
262
- }
283
+ for (const chan of joinedRooms()) watchRoom(chan);
263
284
  }
264
285
  setInterval(checkOnce, POLL_MS);
265
286
 
@@ -271,3 +292,56 @@ function die(msg) {
271
292
  process.stderr.write(`[tmux-pusher] ${msg}\n`);
272
293
  process.exit(1);
273
294
  }
295
+
296
+ // ---------- channel helpers (mirror src/store.ts) ----------
297
+
298
+ function normalizeRoom(name) {
299
+ if (!name) return DEFAULT_ROOM;
300
+ const n = String(name).trim().replace(/^#+/, "").toLowerCase().replace(/[^a-z0-9._-]/g, "");
301
+ return n || DEFAULT_ROOM;
302
+ }
303
+
304
+ function roomFile(chan) {
305
+ const c = normalizeRoom(chan);
306
+ // c is already normalized to a filesystem-safe charset.
307
+ return c === DEFAULT_ROOM ? path.join(ROOT, "room.jsonl") : path.join(ROOT, "rooms", `${c}.jsonl`);
308
+ }
309
+
310
+ function getRoomOffset(cursor, chan) {
311
+ const c = normalizeRoom(chan);
312
+ return c === DEFAULT_ROOM ? cursor.roomOffset ?? 0 : cursor.roomOffsets?.[c] ?? 0;
313
+ }
314
+
315
+ function setRoomOffset(cursor, chan, n) {
316
+ const c = normalizeRoom(chan);
317
+ if (c === DEFAULT_ROOM) cursor.roomOffset = n;
318
+ else (cursor.roomOffsets ??= {})[c] = n;
319
+ }
320
+
321
+ function joinedRooms() {
322
+ let reg = {};
323
+ try {
324
+ const raw = readFileSync(path.join(ROOT, "rooms.json"), "utf8");
325
+ if (raw.trim()) reg = JSON.parse(raw);
326
+ } catch {
327
+ // no registry yet → just the default channel
328
+ }
329
+ const out = new Set([DEFAULT_ROOM]);
330
+ for (const [chan, e] of Object.entries(reg)) {
331
+ if (e && Array.isArray(e.members) && e.members.includes(AGENT_ID)) out.add(chan);
332
+ }
333
+ return [...out];
334
+ }
335
+
336
+ function watchRoom(chan) {
337
+ const f = roomFile(chan);
338
+ if (watchedRooms.has(f)) return;
339
+ try {
340
+ if (existsSync(f)) {
341
+ watch(f, () => checkOnce());
342
+ watchedRooms.add(f);
343
+ }
344
+ } catch {
345
+ // polling covers it until the file exists
346
+ }
347
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.4.9",
3
+ "version": "0.5.0",
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": {