agent-coord-mcp 0.3.7 → 0.3.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 +89 -9
package/package.json
CHANGED
package/scripts/coord-chat.mjs
CHANGED
|
@@ -80,16 +80,79 @@ function agentColor(id) {
|
|
|
80
80
|
|
|
81
81
|
await register();
|
|
82
82
|
|
|
83
|
+
// Visual separator above the prompt — delimits "typing area starts here."
|
|
84
|
+
// Embedded in the prompt string itself so readline owns the redraw; no
|
|
85
|
+
// scroll-region gymnastics needed (those don't survive tmux cleanly).
|
|
86
|
+
const TTY = !!process.stdout.isTTY;
|
|
87
|
+
let COLS = process.stdout.columns || 80;
|
|
88
|
+
const sepLine = () => A.dim("─".repeat(Math.max(10, COLS)));
|
|
89
|
+
|
|
90
|
+
// inputAreaReady flips to true after the banner + initial drain finish and
|
|
91
|
+
// we lay down the first separator. From that point say() treats the line
|
|
92
|
+
// above the prompt as a separator slot it owns.
|
|
93
|
+
let inputAreaReady = false;
|
|
94
|
+
|
|
95
|
+
if (TTY) {
|
|
96
|
+
process.stdout.on("resize", () => {
|
|
97
|
+
COLS = process.stdout.columns || 80;
|
|
98
|
+
if (typeof rl !== "undefined") {
|
|
99
|
+
rl.setPrompt(makePrompt());
|
|
100
|
+
rl.prompt(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SLASH_COMMANDS = [
|
|
106
|
+
"/dm", "/list", "/who", "/whoami", "/last", "/clear", "/cls",
|
|
107
|
+
"/me", "/help", "/?", "/quit", "/exit",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
function completer(line) {
|
|
111
|
+
// Tab-complete slash commands and DM targets. On multi-match with no
|
|
112
|
+
// common-prefix advancement, surface the options on the first Tab via
|
|
113
|
+
// say() — default readline UX hides them until a second Tab, which most
|
|
114
|
+
// users assume means "nothing happened."
|
|
115
|
+
let hits = [];
|
|
116
|
+
if (line.startsWith("/dm ")) {
|
|
117
|
+
const partial = line.slice(4);
|
|
118
|
+
const reg = readJsonSafe(AGENTS_FILE, {});
|
|
119
|
+
const ids = Object.keys(reg).filter((id) => id !== ID && id.startsWith(partial));
|
|
120
|
+
hits = ids.map((id) => `/dm ${id} `);
|
|
121
|
+
} else if (line.startsWith("/")) {
|
|
122
|
+
hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
123
|
+
}
|
|
124
|
+
if (hits.length > 1 && commonPrefix(hits).length <= line.length) {
|
|
125
|
+
const display = hits.map((h) => h.trim()).join(" ");
|
|
126
|
+
say(A.dim(" ┄ " + display));
|
|
127
|
+
}
|
|
128
|
+
return [hits, line];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function commonPrefix(strs) {
|
|
132
|
+
if (strs.length === 0) return "";
|
|
133
|
+
let p = strs[0];
|
|
134
|
+
for (const s of strs.slice(1)) {
|
|
135
|
+
while (s.indexOf(p) !== 0) p = p.slice(0, -1);
|
|
136
|
+
}
|
|
137
|
+
return p;
|
|
138
|
+
}
|
|
139
|
+
|
|
83
140
|
const rl = readline.createInterface({
|
|
84
141
|
input: process.stdin,
|
|
85
142
|
output: process.stdout,
|
|
86
143
|
prompt: makePrompt(),
|
|
144
|
+
completer,
|
|
87
145
|
});
|
|
88
146
|
|
|
89
147
|
// Banner — printed once on launch. Keep it tight; this is a CLI, not a poster.
|
|
90
148
|
printBanner();
|
|
91
149
|
await drainAndPrint();
|
|
92
150
|
|
|
151
|
+
// Lay down the first separator, then activate input-area mode so subsequent
|
|
152
|
+
// say() calls maintain a sep line directly above the prompt.
|
|
153
|
+
process.stdout.write(sepLine() + "\n");
|
|
154
|
+
inputAreaReady = true;
|
|
155
|
+
|
|
93
156
|
try { watch(INBOX_FILE, () => void drainAndPrint()); } catch {}
|
|
94
157
|
try { watch(ROOM_FILE, () => void drainAndPrint()); } catch {}
|
|
95
158
|
try { watch(AGENTS_FILE, () => refreshPrompt()); } catch {}
|
|
@@ -104,7 +167,8 @@ rl.on("line", async (line) => {
|
|
|
104
167
|
try {
|
|
105
168
|
if (text === "/quit" || text === "/exit") {
|
|
106
169
|
await unregister();
|
|
107
|
-
|
|
170
|
+
teardownFooter();
|
|
171
|
+
process.stdout.write(A.dim("bye.\n"));
|
|
108
172
|
process.exit(0);
|
|
109
173
|
} else if (text === "/help" || text === "/?") {
|
|
110
174
|
printHelp();
|
|
@@ -135,16 +199,24 @@ rl.on("line", async (line) => {
|
|
|
135
199
|
} catch (e) {
|
|
136
200
|
say(A.red(`error: ${e?.message ?? e}`));
|
|
137
201
|
}
|
|
202
|
+
// After Enter, terminal advanced to a new line. Print a fresh separator
|
|
203
|
+
// there so the next prompt sits below a sep line, maintaining the layout.
|
|
204
|
+
process.stdout.write(sepLine() + "\n");
|
|
138
205
|
rl.prompt();
|
|
139
206
|
});
|
|
140
207
|
|
|
141
208
|
process.on("SIGINT", async () => {
|
|
142
209
|
try { await unregister(); } catch {}
|
|
143
|
-
|
|
144
|
-
|
|
210
|
+
teardownFooter();
|
|
211
|
+
process.stdout.write("\n" + A.dim("bye.\n"));
|
|
145
212
|
process.exit(0);
|
|
146
213
|
});
|
|
147
214
|
|
|
215
|
+
process.on("exit", () => {
|
|
216
|
+
// Final safety net — restore terminal state if we exit via any path.
|
|
217
|
+
teardownFooter();
|
|
218
|
+
});
|
|
219
|
+
|
|
148
220
|
// ---------- helpers ----------
|
|
149
221
|
|
|
150
222
|
function parseArgs(argv) {
|
|
@@ -166,11 +238,17 @@ function sanitize(s) {
|
|
|
166
238
|
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
167
239
|
}
|
|
168
240
|
|
|
169
|
-
// Write output without clobbering whatever the user is typing.
|
|
170
241
|
function say(line) {
|
|
171
|
-
|
|
172
|
-
|
|
242
|
+
if (!inputAreaReady) {
|
|
243
|
+
process.stdout.write(line + "\n");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// We're on the prompt line. The line above is the current separator.
|
|
247
|
+
// Move up to it, clear it, drop our message there, then a fresh separator,
|
|
248
|
+
// then re-render the prompt on the next line.
|
|
249
|
+
process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
173
250
|
process.stdout.write(line + "\n");
|
|
251
|
+
process.stdout.write(sepLine() + "\n");
|
|
174
252
|
if (typeof rl !== "undefined") rl.prompt(true);
|
|
175
253
|
}
|
|
176
254
|
|
|
@@ -190,6 +268,9 @@ function makePrompt() {
|
|
|
190
268
|
return `${agentColor(ID)(ID)} ${A.dim(`(${peers})`)}${A.dim(">")} `;
|
|
191
269
|
}
|
|
192
270
|
|
|
271
|
+
// No-op stubs kept so the exit paths don't reference deleted functions.
|
|
272
|
+
function teardownFooter() {}
|
|
273
|
+
|
|
193
274
|
function refreshPrompt() {
|
|
194
275
|
if (typeof rl === "undefined") return;
|
|
195
276
|
rl.setPrompt(makePrompt());
|
|
@@ -197,15 +278,14 @@ function refreshPrompt() {
|
|
|
197
278
|
}
|
|
198
279
|
|
|
199
280
|
function printBanner() {
|
|
200
|
-
// Compact banner — three lines plus a separator
|
|
201
|
-
//
|
|
281
|
+
// Compact banner — three lines plus a separator matching the input-area
|
|
282
|
+
// separator width so they look like the same UI element, not two.
|
|
202
283
|
const lines = [
|
|
203
284
|
A.bold(A.cyan(" agent-coord ")) + A.dim("— shared chat for agents and humans"),
|
|
204
285
|
A.dim(` agentId=${A.reset}${agentColor(ID)(ID)}${A.dim(" dir=" + ROOT)}`),
|
|
205
286
|
A.dim(" type /help for commands · /quit to leave"),
|
|
206
287
|
];
|
|
207
288
|
for (const l of lines) say(l);
|
|
208
|
-
say(A.dim(" " + "─".repeat(60)));
|
|
209
289
|
}
|
|
210
290
|
|
|
211
291
|
function printHelp() {
|