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 +1 -1
- package/scripts/coord-chat.mjs +72 -27
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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) => {
|