agent-coord-mcp 0.4.0 → 0.4.3
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 +135 -32
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -67,11 +67,19 @@ 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 from agentId hash.
|
|
73
|
-
//
|
|
74
|
-
const AGENT_COLORS = [
|
|
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.
|
|
79
|
+
const AGENT_COLORS = [
|
|
80
|
+
A.green, A.yellow, A.blue, A.magenta, A.cyan,
|
|
81
|
+
A.brightGreen, A.brightYellow, A.brightBlue, A.brightMagenta, A.brightCyan,
|
|
82
|
+
];
|
|
75
83
|
function agentColor(id) {
|
|
76
84
|
let h = 0;
|
|
77
85
|
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
|
|
@@ -429,23 +437,92 @@ async function postStatus(status) {
|
|
|
429
437
|
async function pruneOld(days) {
|
|
430
438
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
431
439
|
let total = 0;
|
|
432
|
-
|
|
440
|
+
// Per-file removal counts so we can shift the corresponding cursor
|
|
441
|
+
// offsets — without this, every other agent's roomOffset/statusOffset/
|
|
442
|
+
// inboxOffset would point past the now-shorter file and they'd silently
|
|
443
|
+
// miss messages until enough new ones piled up to overtake the stale offset.
|
|
444
|
+
let roomRemoved = 0;
|
|
445
|
+
let statusRemoved = 0;
|
|
446
|
+
const inboxRemoved = {}; // agentId → count
|
|
447
|
+
|
|
448
|
+
const files = [
|
|
449
|
+
{ path: ROOM_FILE, kind: "room" },
|
|
450
|
+
{ path: STATUS_FILE_PATH, kind: "status" },
|
|
451
|
+
];
|
|
433
452
|
if (existsSync(INBOX_DIR)) {
|
|
434
453
|
for (const n of readdirSync(INBOX_DIR)) {
|
|
435
|
-
if (n.endsWith(".jsonl"))
|
|
454
|
+
if (n.endsWith(".jsonl")) {
|
|
455
|
+
files.push({ path: path.join(INBOX_DIR, n), kind: "inbox", agentId: n.replace(/\.jsonl$/, "") });
|
|
456
|
+
}
|
|
436
457
|
}
|
|
437
458
|
}
|
|
438
|
-
for (const
|
|
439
|
-
if (!existsSync(
|
|
440
|
-
const all = readJsonl(
|
|
459
|
+
for (const f of files) {
|
|
460
|
+
if (!existsSync(f.path)) continue;
|
|
461
|
+
const all = readJsonl(f.path);
|
|
441
462
|
const kept = all.filter((e) => e && e.ts > cutoff);
|
|
442
|
-
|
|
463
|
+
const removed = all.length - kept.length;
|
|
464
|
+
if (removed > 0) {
|
|
443
465
|
const body = kept.length ? kept.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
|
|
444
|
-
writeFileSync(
|
|
445
|
-
total +=
|
|
466
|
+
writeFileSync(f.path, body);
|
|
467
|
+
total += removed;
|
|
468
|
+
if (f.kind === "room") roomRemoved += removed;
|
|
469
|
+
else if (f.kind === "status") statusRemoved += removed;
|
|
470
|
+
else inboxRemoved[f.agentId] = (inboxRemoved[f.agentId] ?? 0) + removed;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (roomRemoved || statusRemoved || Object.keys(inboxRemoved).length) {
|
|
474
|
+
shiftAllCursors({ roomRemoved, statusRemoved, inboxRemoved });
|
|
475
|
+
}
|
|
476
|
+
say(A.dim(`→ pruned ${total} entries older than ${days}d (cursors adjusted)`));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function wipeRoom() {
|
|
480
|
+
await ensureFile(ROOM_FILE);
|
|
481
|
+
writeFileSync(ROOM_FILE, "");
|
|
482
|
+
// Reset every agent's roomOffset to 0 so they start reading the (now empty)
|
|
483
|
+
// file from the beginning. Otherwise their stale offsets point past EOF.
|
|
484
|
+
resetAllRoomOffsets();
|
|
485
|
+
say(A.dim("→ room wiped (all room cursors reset)"));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Walk every cursor file and shift offsets down by the per-channel removed
|
|
489
|
+
// counts. Mirrors what the MCP prune tool does server-side.
|
|
490
|
+
function shiftAllCursors({ roomRemoved = 0, statusRemoved = 0, inboxRemoved = {} }) {
|
|
491
|
+
if (!existsSync(CURSOR_DIR)) return;
|
|
492
|
+
for (const name of readdirSync(CURSOR_DIR)) {
|
|
493
|
+
if (!name.endsWith(".json")) continue;
|
|
494
|
+
const cursorPath = path.join(CURSOR_DIR, name);
|
|
495
|
+
const cur = readJsonSafe(cursorPath, {});
|
|
496
|
+
const id = name.replace(/\.json$/, "");
|
|
497
|
+
let touched = false;
|
|
498
|
+
if (cur.roomOffset !== undefined && roomRemoved > 0) {
|
|
499
|
+
cur.roomOffset = Math.max(0, cur.roomOffset - roomRemoved);
|
|
500
|
+
touched = true;
|
|
501
|
+
}
|
|
502
|
+
if (cur.statusOffset !== undefined && statusRemoved > 0) {
|
|
503
|
+
cur.statusOffset = Math.max(0, cur.statusOffset - statusRemoved);
|
|
504
|
+
touched = true;
|
|
505
|
+
}
|
|
506
|
+
const myInbox = inboxRemoved[id] ?? 0;
|
|
507
|
+
if (cur.inboxOffset !== undefined && myInbox > 0) {
|
|
508
|
+
cur.inboxOffset = Math.max(0, cur.inboxOffset - myInbox);
|
|
509
|
+
touched = true;
|
|
510
|
+
}
|
|
511
|
+
if (touched) writeJsonAtomic(cursorPath, cur);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function resetAllRoomOffsets() {
|
|
516
|
+
if (!existsSync(CURSOR_DIR)) return;
|
|
517
|
+
for (const name of readdirSync(CURSOR_DIR)) {
|
|
518
|
+
if (!name.endsWith(".json")) continue;
|
|
519
|
+
const cursorPath = path.join(CURSOR_DIR, name);
|
|
520
|
+
const cur = readJsonSafe(cursorPath, {});
|
|
521
|
+
if (cur.roomOffset !== undefined && cur.roomOffset !== 0) {
|
|
522
|
+
cur.roomOffset = 0;
|
|
523
|
+
writeJsonAtomic(cursorPath, cur);
|
|
446
524
|
}
|
|
447
525
|
}
|
|
448
|
-
say(A.dim(`→ pruned ${total} entries older than ${days}d`));
|
|
449
526
|
}
|
|
450
527
|
|
|
451
528
|
async function kickAgent(target) {
|
|
@@ -471,19 +548,13 @@ async function kickAgent(target) {
|
|
|
471
548
|
try { process.kill(marker.pid, "SIGTERM"); } catch {}
|
|
472
549
|
}
|
|
473
550
|
try { if (existsSync(markerPath)) unlinkSync(markerPath); } catch {}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const cur = readJsonSafe(CURSOR_FILE, {});
|
|
482
|
-
if (cur.roomOffset !== undefined) {
|
|
483
|
-
delete cur.roomOffset;
|
|
484
|
-
writeJsonAtomic(CURSOR_FILE, cur);
|
|
485
|
-
}
|
|
486
|
-
say(A.dim("→ room wiped"));
|
|
551
|
+
// Remove the kicked agent's inbox + cursor so they don't sit orphaned in
|
|
552
|
+
// ~/agent-coord/ taking up listing space and confusing future bookkeeping.
|
|
553
|
+
const inboxPath = path.join(INBOX_DIR, `${sanitize(target)}.jsonl`);
|
|
554
|
+
const cursorPath = path.join(CURSOR_DIR, `${sanitize(target)}.json`);
|
|
555
|
+
try { if (existsSync(inboxPath)) unlinkSync(inboxPath); } catch {}
|
|
556
|
+
try { if (existsSync(cursorPath)) unlinkSync(cursorPath); } catch {}
|
|
557
|
+
say(A.dim(`→ kicked ${target} (registry, transport, inbox, cursor all cleared)`));
|
|
487
558
|
}
|
|
488
559
|
|
|
489
560
|
async function findInHistory(term) {
|
|
@@ -541,19 +612,51 @@ function printMsg(kind, m, opts = {}) {
|
|
|
541
612
|
const d = new Date(m.ts);
|
|
542
613
|
const t = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
543
614
|
const who = m.from ?? "?";
|
|
615
|
+
const color = agentColor(who);
|
|
616
|
+
const gutter = color("▎");
|
|
544
617
|
const tag = kind === "DM" ? A.bold(A.cyan("DM")) : A.dim("room");
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const indent = " ";
|
|
618
|
+
const nick = A.bold(color(who));
|
|
619
|
+
const meta = `${A.dim(t)} ${tag} ${nick}`;
|
|
620
|
+
const body = (m.text ?? "").split("\n").map(formatBody);
|
|
621
|
+
const indent = " ";
|
|
550
622
|
const first = body[0] ?? "";
|
|
551
|
-
const rest = body.slice(1).map((l) => indent
|
|
623
|
+
const rest = body.slice(1).map((l) => `${gutter} ${indent}${A.dim("│ ")}${l}`);
|
|
552
624
|
const prefix = opts.history ? A.dim(" ") : "";
|
|
553
|
-
say(`${prefix}${meta} ${first}`);
|
|
625
|
+
say(`${prefix}${gutter} ${meta} ${first}`);
|
|
554
626
|
for (const line of rest) say(`${prefix}${line}`);
|
|
555
627
|
}
|
|
556
628
|
|
|
629
|
+
// Lightweight inline-only "chat markdown" formatter — no dep. Handles bold,
|
|
630
|
+
// italic, inline code, links, and @mentions. Order matters: pull out inline
|
|
631
|
+
// code spans first so we don't touch their contents, then run the rest.
|
|
632
|
+
function formatBody(text) {
|
|
633
|
+
return text.split(/(`[^`\n]+`)/).map((part) => {
|
|
634
|
+
if (part.startsWith("`") && part.endsWith("`") && part.length >= 2) {
|
|
635
|
+
return A.dim("`") + A.cyan(part.slice(1, -1)) + A.dim("`");
|
|
636
|
+
}
|
|
637
|
+
let s = part;
|
|
638
|
+
// @mentions first — colored in the mentioned agent's hash color, bold if
|
|
639
|
+
// it's the current user (so you can spot pings at a glance).
|
|
640
|
+
s = s.replace(/@([A-Za-z0-9._-]+)/g, (_, name) => {
|
|
641
|
+
const colored = agentColor(name)(`@${name}`);
|
|
642
|
+
return name === ID ? A.bold(colored) : colored;
|
|
643
|
+
});
|
|
644
|
+
// **bold**
|
|
645
|
+
s = s.replace(/\*\*([^*\n]+)\*\*/g, (_, t) => A.bold(t));
|
|
646
|
+
// *italic* and _italic_ (avoid matching inside **bold** by requiring
|
|
647
|
+
// non-asterisk neighbors)
|
|
648
|
+
s = s.replace(/(?<![*\w])\*([^*\n]+)\*(?![*\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
|
|
649
|
+
s = s.replace(/(?<![_\w])_([^_\n]+)_(?![_\w])/g, (_, t) => `\x1b[3m${t}\x1b[0m`);
|
|
650
|
+
// [text](url) — show text underlined with a dim trailing (url)
|
|
651
|
+
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, (_, t, u) =>
|
|
652
|
+
`\x1b[4m${t}\x1b[0m${A.dim(` (${u})`)}`,
|
|
653
|
+
);
|
|
654
|
+
// Bare URLs — underline only the URL itself
|
|
655
|
+
s = s.replace(/\bhttps?:\/\/[^\s)]+/g, (u) => `\x1b[4m${u}\x1b[0m`);
|
|
656
|
+
return s;
|
|
657
|
+
}).join("");
|
|
658
|
+
}
|
|
659
|
+
|
|
557
660
|
async function printAgents() {
|
|
558
661
|
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
559
662
|
const now = Date.now();
|