agent-coord-mcp 0.25.1 → 0.25.2
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/dist/server.js +1 -1
- package/hooks/submit.mjs +66 -1
- package/package.json +1 -1
- package/scripts/check-test-count.mjs +9 -3
- package/src/server.ts +1 -1
package/dist/server.js
CHANGED
|
@@ -160,7 +160,7 @@ function buildServer(initialBound, opts = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
const server = new McpServer({
|
|
162
162
|
name: "agent-coord",
|
|
163
|
-
version: "0.25.
|
|
163
|
+
version: "0.25.2",
|
|
164
164
|
});
|
|
165
165
|
const addTool = (name, description, inputSchema, cb) => {
|
|
166
166
|
server.registerTool(name, { description, inputSchema: z.object(inputSchema) }, async (args) => cb((args ?? {})));
|
package/hooks/submit.mjs
CHANGED
|
@@ -38,6 +38,13 @@ export const ENTER_GAP_MS = () => envInt("AGENT_COORD_ENTER_GAP_MS", 150);
|
|
|
38
38
|
// often. Bounded and finite — this must not spin, and it must not stall the
|
|
39
39
|
// delivery loop behind a pane that will never change.
|
|
40
40
|
export const VERIFY_TIMEOUT_MS = () => envInt("AGENT_COORD_SUBMIT_VERIFY_MS", 1500);
|
|
41
|
+
// Ceiling for the paste-landing check: how long after paste-buffer we keep
|
|
42
|
+
// polling for the pasted text to APPEAR in the input line before refusing to
|
|
43
|
+
// send Enter. ENTER_DELAY_MS stays the floor (menu-settle reasoning above) —
|
|
44
|
+
// landing early never shortens it. On a slow machine the paste can take
|
|
45
|
+
// longer than any fixed delay; a blind Enter then submits whatever WAS in the
|
|
46
|
+
// input (nothing, or someone's draft) while the real payload lands after it.
|
|
47
|
+
export const PASTE_VERIFY_MS = () => envInt("AGENT_COORD_PASTE_VERIFY_MS", 4000);
|
|
41
48
|
export const VERIFY_POLL_MS = () => envInt("AGENT_COORD_SUBMIT_POLL_MS", 100);
|
|
42
49
|
// Extra Enters to try when the first pair didn't submit. 0 disables retrying.
|
|
43
50
|
export const ENTER_RETRIES = () => envInt("AGENT_COORD_ENTER_RETRIES", 2);
|
|
@@ -187,6 +194,25 @@ export function stillInInput(paneText, payload) {
|
|
|
187
194
|
return squash(draft).includes(needle);
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
// Has the PASTED payload appeared in the input line yet? Distinct from
|
|
198
|
+
// stillInInput (verify-by-absence after Enter): before Enter, "gone" cannot
|
|
199
|
+
// mean submitted — it means not-landed-yet. The input box wraps long pastes
|
|
200
|
+
// across lines the prompt regex does not match, so the visible draft may be
|
|
201
|
+
// only the head of the payload; match on a shared prefix in either direction.
|
|
202
|
+
// true = payload (or its head) is visibly in the input; false = input readable
|
|
203
|
+
// and payload not there; null = no readable input line (unknown TUI/capture).
|
|
204
|
+
export function landedInInput(paneText, payload) {
|
|
205
|
+
if (paneText === null || paneText === undefined) return null;
|
|
206
|
+
const needle = squash(payload);
|
|
207
|
+
if (!needle) return false;
|
|
208
|
+
const { draft } = readPaneState(paneText);
|
|
209
|
+
if (draft === null) return null;
|
|
210
|
+
const d = squash(draft);
|
|
211
|
+
if (!d) return false;
|
|
212
|
+
if (d.includes(needle.slice(0, 32))) return true; // draft carries the payload head
|
|
213
|
+
return needle.includes(d) && d.length >= 8; // visible draft is a genuine fragment of the payload
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
// Read the pane's input state: is the TUI busy, and is there unsent text?
|
|
191
217
|
//
|
|
192
218
|
// Accepts BOTH plain and styled (`capture-pane -e`) text — on styled input the
|
|
@@ -323,7 +349,30 @@ export async function pasteAndSubmit(deps, payload, { bracketed = false, verify
|
|
|
323
349
|
throw new Error(`tmux paste-buffer: ${String(paste.stderr ?? "").trim()}`);
|
|
324
350
|
}
|
|
325
351
|
|
|
326
|
-
|
|
352
|
+
// Verify the paste LANDED before any Enter (recurring loss on slow machines:
|
|
353
|
+
// the blind delay elapsed before the paste rendered, and the Enter submitted
|
|
354
|
+
// an empty input while the payload arrived after it). ENTER_DELAY_MS is the
|
|
355
|
+
// floor; PASTE_VERIFY_MS is the ceiling. Three outcomes:
|
|
356
|
+
// true — payload seen in the input; proceed to Enter.
|
|
357
|
+
// false — input line readable the whole time and the payload never
|
|
358
|
+
// appeared; refuse the Enter and say so (honest timeout, not a
|
|
359
|
+
// blind CR into someone's session).
|
|
360
|
+
// null — no readable input line (unknown TUI / capture failed); fall back
|
|
361
|
+
// to the legacy fixed-delay path — unknown never blocks delivery.
|
|
362
|
+
const floor = sleep(ENTER_DELAY_MS());
|
|
363
|
+
const landed = await pollUntilLanded(deps, payload);
|
|
364
|
+
await floor;
|
|
365
|
+
if (landed === false) {
|
|
366
|
+
return {
|
|
367
|
+
submitted: false,
|
|
368
|
+
verified: true, // we DID observe the input line; what we observed is a paste that never arrived
|
|
369
|
+
attempts: 0,
|
|
370
|
+
reason:
|
|
371
|
+
`pasted text did not appear in the input line of pane '${target}' within ${PASTE_VERIFY_MS()}ms — ` +
|
|
372
|
+
`Enter NOT sent (the paste may still land later; raise AGENT_COORD_PASTE_VERIFY_MS on slow machines, ` +
|
|
373
|
+
`and check the pane for a stranded draft before resending)`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
327
376
|
const e1 = run(["send-keys", "-t", target, "Enter"]);
|
|
328
377
|
if (e1.status !== 0) throw new Error(`tmux send-keys: ${String(e1.stderr ?? "").trim()}`);
|
|
329
378
|
await sleep(ENTER_GAP_MS());
|
|
@@ -369,6 +418,22 @@ export async function pasteAndSubmit(deps, payload, { bracketed = false, verify
|
|
|
369
418
|
}
|
|
370
419
|
}
|
|
371
420
|
|
|
421
|
+
// Poll capture-pane until the pasted payload APPEARS in the input line, the
|
|
422
|
+
// PASTE_VERIFY_MS budget runs out, or the input line proves unreadable.
|
|
423
|
+
// true = landed, false = readable-but-never-landed, null = never readable.
|
|
424
|
+
async function pollUntilLanded(deps, payload) {
|
|
425
|
+
const { run, target } = deps;
|
|
426
|
+
const deadline = Date.now() + PASTE_VERIFY_MS();
|
|
427
|
+
let readable = false;
|
|
428
|
+
for (;;) {
|
|
429
|
+
const verdict = landedInInput(captureStyled(run, target), payload);
|
|
430
|
+
if (verdict === true) return true;
|
|
431
|
+
if (verdict === false) readable = true;
|
|
432
|
+
if (Date.now() >= deadline) return readable ? false : null;
|
|
433
|
+
await sleep(VERIFY_POLL_MS());
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
372
437
|
// Poll capture-pane until the payload leaves the input line, the budget runs
|
|
373
438
|
// out, or we run out of ways to tell. true = still there, false = gone,
|
|
374
439
|
// null = unknown (capture failed, or no input line we can read).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-coord-mcp",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "File-backed MCP server for coordinating multiple AI coding agents (Claude Code, Cursor, Cline, etc.). Local stdio or networked over Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
|
|
25
|
-
const EXPECTED_TESTS =
|
|
25
|
+
const EXPECTED_TESTS = 285;
|
|
26
26
|
|
|
27
27
|
const expected = Number(process.env.AGENT_COORD_EXPECTED_TESTS ?? EXPECTED_TESTS);
|
|
28
28
|
// Same glob the suite always used — `--test test/` would recurse differently
|
|
29
29
|
// on some node versions, and a runner that selects a different set of files
|
|
30
30
|
// is exactly the failure this script exists to catch.
|
|
31
|
-
|
|
31
|
+
// Reporter pinned to TAP: newer Node defaults to the spec reporter even when
|
|
32
|
+
// piped (and env like FORCE_COLOR can style its summary lines), so grepping
|
|
33
|
+
// the summary was machine-dependent — it broke David's publish run 2026-08-20.
|
|
34
|
+
// TAP is stable, uncolored, and the format the totals-check below was written
|
|
35
|
+
// against.
|
|
36
|
+
const child = spawn(process.execPath, ["--test", "--test-reporter=tap", "test/*.test.mjs"], {
|
|
32
37
|
stdio: ["inherit", "pipe", "inherit"],
|
|
33
38
|
shell: false,
|
|
34
39
|
});
|
|
@@ -41,7 +46,8 @@ child.stdout.on("data", (chunk) => {
|
|
|
41
46
|
|
|
42
47
|
child.on("exit", (code, signal) => {
|
|
43
48
|
if (signal) process.exit(1);
|
|
44
|
-
// TAP
|
|
49
|
+
// TAP is forced above, so `# pass N` is the contract; the `ℹ` alternative
|
|
50
|
+
// stays as a belt against a runner that ignores the flag.
|
|
45
51
|
const num = (label) => {
|
|
46
52
|
const m = out.match(new RegExp(`^(?:#|ℹ) ${label} (\\d+)\\s*$`, "m"));
|
|
47
53
|
return m ? Number(m[1]) : null;
|
package/src/server.ts
CHANGED