agent-coord-mcp 0.4.7 → 0.4.9
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 +111 -29
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -44,6 +44,38 @@ 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
|
+
// Whether the ephemeral suggestion hint is currently occupying the slot above
|
|
58
|
+
// the prompt (see drawHint/clearHint). Declared early so say(), called during
|
|
59
|
+
// startup replay, can reference it without tripping the const/let TDZ.
|
|
60
|
+
let hintActive = false;
|
|
61
|
+
|
|
62
|
+
// Matches "@<this agent>" not followed by a name char, so we can flag messages
|
|
63
|
+
// that ping the current user. ID may contain regex metachars — escape it.
|
|
64
|
+
const SELF_MENTION_RE = new RegExp(
|
|
65
|
+
"@" + ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![A-Za-z0-9._-])",
|
|
66
|
+
);
|
|
67
|
+
const mentionsSelf = (text) => SELF_MENTION_RE.test(text ?? "");
|
|
68
|
+
|
|
69
|
+
// Recency at a glance: "now" / "5m" for fresh messages, falling back to a wall
|
|
70
|
+
// clock for anything over an hour (a stale "63m" reads worse than "08:34").
|
|
71
|
+
function relTime(ts) {
|
|
72
|
+
const mins = Math.floor((Date.now() - ts) / 60000);
|
|
73
|
+
if (mins < 1) return "now";
|
|
74
|
+
if (mins < 60) return `${mins}m`;
|
|
75
|
+
const d = new Date(ts);
|
|
76
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
const INBOX_DIR = path.join(ROOT, "inbox");
|
|
48
80
|
const CURSOR_DIR = path.join(ROOT, "cursors");
|
|
49
81
|
const TRANSPORT_DIR = path.join(ROOT, "transports");
|
|
@@ -136,21 +168,24 @@ function completer(line) {
|
|
|
136
168
|
const mentionMatch = line.match(/@([A-Za-z0-9._-]*)$/);
|
|
137
169
|
if (mentionMatch) {
|
|
138
170
|
const partial = mentionMatch[1];
|
|
139
|
-
const
|
|
140
|
-
const ids = Object.keys(reg).filter((id) => id.startsWith(partial));
|
|
171
|
+
const ids = onlineAgentIds().filter((id) => id !== ID && id.startsWith(partial));
|
|
141
172
|
hits = ids.map((id) => `@${id} `);
|
|
142
173
|
substr = mentionMatch[0]; // tell readline to replace just the @partial part
|
|
143
174
|
} else if (line.startsWith("/dm ")) {
|
|
144
175
|
const partial = line.slice(4);
|
|
145
|
-
const
|
|
146
|
-
const ids = Object.keys(reg).filter((id) => id !== ID && id.startsWith(partial));
|
|
176
|
+
const ids = onlineAgentIds().filter((id) => id !== ID && id.startsWith(partial));
|
|
147
177
|
hits = ids.map((id) => `/dm ${id} `);
|
|
148
178
|
} else if (line.startsWith("/")) {
|
|
149
179
|
hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
150
180
|
}
|
|
151
|
-
if (hits.length > 1
|
|
181
|
+
if (hits.length > 1) {
|
|
182
|
+
// Show the options as an ephemeral hint and hand readline only the common
|
|
183
|
+
// prefix — a single-element completion means its native multi-column dump
|
|
184
|
+
// never lands in scrollback. Tab still advances to the shared prefix.
|
|
152
185
|
const display = hits.map((h) => h.trim()).join(" ");
|
|
153
|
-
|
|
186
|
+
drawHint(A.dim(" ┄ " + display));
|
|
187
|
+
const cp = commonPrefix(hits);
|
|
188
|
+
return [[cp.length >= substr.length ? cp : substr], substr];
|
|
154
189
|
}
|
|
155
190
|
return [hits, substr];
|
|
156
191
|
}
|
|
@@ -171,6 +206,23 @@ const rl = readline.createInterface({
|
|
|
171
206
|
completer,
|
|
172
207
|
});
|
|
173
208
|
|
|
209
|
+
// Auto-offer the logged-in agents the instant "@" is typed (editor-style),
|
|
210
|
+
// so you don't have to press Tab to discover who's reachable. We only observe
|
|
211
|
+
// keypresses — readline still owns input. setImmediate lets readline insert
|
|
212
|
+
// the "@" into its line buffer before we inspect it.
|
|
213
|
+
if (process.stdin.isTTY) {
|
|
214
|
+
readline.emitKeypressEvents(process.stdin);
|
|
215
|
+
process.stdin.on("keypress", (str, key) => {
|
|
216
|
+
// setImmediate lets readline mutate its line buffer first, then we inspect
|
|
217
|
+
// it / redraw the slot above the prompt.
|
|
218
|
+
if (str === "@") { setImmediate(showMentionPicker); return; }
|
|
219
|
+
// Tab is the completer's — it draws/keeps the hint itself. Every other key
|
|
220
|
+
// dismisses a showing hint so the view snaps back to a clean separator.
|
|
221
|
+
if (key && key.name === "tab") return;
|
|
222
|
+
if (hintActive) setImmediate(clearHint);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
174
226
|
// Banner — printed once on launch. Keep it tight; this is a CLI, not a poster.
|
|
175
227
|
printBanner();
|
|
176
228
|
// Show recent context (last 3 messages from inbox + room) then fast-forward
|
|
@@ -322,7 +374,28 @@ function agentColor(id) {
|
|
|
322
374
|
return AGENT_COLORS[idx];
|
|
323
375
|
}
|
|
324
376
|
|
|
377
|
+
// Ephemeral suggestion line (the @mention / completion picker). It borrows the
|
|
378
|
+
// separator slot directly above the prompt: drawn there, then wiped back to a
|
|
379
|
+
// real separator on the next keystroke — so it never piles up in scrollback.
|
|
380
|
+
function drawHint(content) {
|
|
381
|
+
if (typeof rl === "undefined" || !lastLineWasSep) return;
|
|
382
|
+
process.stdout.write("\x1b[1A\r\x1b[2K"); // up to the slot, clear it
|
|
383
|
+
process.stdout.write(content + "\n"); // draw the hint in place of the sep
|
|
384
|
+
rl.prompt(true); // redraw prompt + preserved input
|
|
385
|
+
hintActive = true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function clearHint() {
|
|
389
|
+
if (typeof rl === "undefined" || !lastLineWasSep) { hintActive = false; return; }
|
|
390
|
+
if (!hintActive) return;
|
|
391
|
+
process.stdout.write("\x1b[1A\r\x1b[2K"); // up to the hint slot, clear it
|
|
392
|
+
process.stdout.write(sepLine() + "\n"); // restore the separator
|
|
393
|
+
rl.prompt(true);
|
|
394
|
+
hintActive = false;
|
|
395
|
+
}
|
|
396
|
+
|
|
325
397
|
function say(line) {
|
|
398
|
+
hintActive = false; // any real output reclaims the slot the hint borrowed
|
|
326
399
|
if (lastLineWasSep) {
|
|
327
400
|
// Async path: a separator we own sits directly above the prompt; we own
|
|
328
401
|
// that line. Replace it with the incoming message, drop a new sep, and
|
|
@@ -661,29 +734,6 @@ async function drainAndPrint() {
|
|
|
661
734
|
if (changed) writeJsonAtomic(CURSOR_FILE, cursor);
|
|
662
735
|
}
|
|
663
736
|
|
|
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
737
|
function printMsg(kind, m, opts = {}) {
|
|
688
738
|
const who = m.from ?? "?";
|
|
689
739
|
const color = agentColor(who);
|
|
@@ -836,6 +886,38 @@ function pidAlive(pid) {
|
|
|
836
886
|
catch (e) { return e?.code === "EPERM"; }
|
|
837
887
|
}
|
|
838
888
|
|
|
889
|
+
// Agents considered "logged in": a live transport process, or a heartbeat
|
|
890
|
+
// within the stale window. Shared by the @mention picker and completer so we
|
|
891
|
+
// only ever offer reachable agents.
|
|
892
|
+
function onlineAgentIds() {
|
|
893
|
+
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
894
|
+
const now = Date.now();
|
|
895
|
+
const STALE = 5 * 60 * 1000;
|
|
896
|
+
return Object.keys(reg)
|
|
897
|
+
.filter((id) => {
|
|
898
|
+
const a = reg[id];
|
|
899
|
+
const marker = readJsonSafe(path.join(TRANSPORT_DIR, `${sanitize(id)}.json`), null);
|
|
900
|
+
const live = marker && marker.pid && pidAlive(marker.pid);
|
|
901
|
+
return live || now - (a?.lastHeartbeat ?? 0) < STALE;
|
|
902
|
+
})
|
|
903
|
+
.sort();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Pop the list of logged-in agents the moment "@" starts a mention token, so
|
|
907
|
+
// you can see who's reachable without hunting through /list. The list is
|
|
908
|
+
// dim/cosmetic and re-renders above the preserved input line.
|
|
909
|
+
function showMentionPicker() {
|
|
910
|
+
if (typeof rl === "undefined") return;
|
|
911
|
+
const before = (rl.line ?? "").slice(0, rl.cursor ?? (rl.line ?? "").length);
|
|
912
|
+
// Only when the just-typed "@" opens a fresh token (start of line or after
|
|
913
|
+
// whitespace) — avoids firing inside emails or mid-word.
|
|
914
|
+
if (!/(^|\s)@$/.test(before)) return;
|
|
915
|
+
const ids = onlineAgentIds().filter((id) => id !== ID);
|
|
916
|
+
if (!ids.length) return;
|
|
917
|
+
const list = ids.map((id) => A.green("●") + agentColor(id)(`@${id}`)).join(" ");
|
|
918
|
+
drawHint(A.dim(" ┄ ") + list + A.dim(" · Tab to complete"));
|
|
919
|
+
}
|
|
920
|
+
|
|
839
921
|
function readJsonl(file) {
|
|
840
922
|
if (!existsSync(file)) return [];
|
|
841
923
|
return readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => {
|