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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-coord-mcp",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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": {
@@ -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
- say(A.dim("bye."));
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
- process.stdout.write("\n");
144
- say("bye.");
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
- readline.clearLine(process.stdout, 0);
172
- readline.cursorTo(process.stdout, 0);
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. ASCII so it renders
201
- // anywhere; no UTF-8 box-drawing surprises.
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() {