agent-comm-hub 0.7.1 → 0.8.0
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/README.md +1 -1
- package/agents/SKILL.md +5 -2
- package/assets/admin.html +526 -16
- package/lib/cli.js +206 -23
- package/lib/index.js +202 -21
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -422,6 +422,7 @@ import { randomUUID } from "node:crypto";
|
|
|
422
422
|
var KINDS = ["chat", "task", "notice", "ack"];
|
|
423
423
|
var PEER_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
424
424
|
var BROADCAST = "all";
|
|
425
|
+
var TERMINAL_TASK_STATUSES = ["rejected", "done", "failed"];
|
|
425
426
|
function encodeContent(payload) {
|
|
426
427
|
return JSON.stringify(payload);
|
|
427
428
|
}
|
|
@@ -437,6 +438,7 @@ function decodeContent(kind, content) {
|
|
|
437
438
|
|
|
438
439
|
// src/hub.ts
|
|
439
440
|
var MAX_PROFILES = 512;
|
|
441
|
+
var MAX_TASKS = 1024;
|
|
440
442
|
var AgentHub = class {
|
|
441
443
|
constructor(options) {
|
|
442
444
|
this.options = options;
|
|
@@ -455,6 +457,8 @@ var AgentHub = class {
|
|
|
455
457
|
profiles = /* @__PURE__ */ new Map();
|
|
456
458
|
/** Named channels (group channel), keyed by group id. */
|
|
457
459
|
groups = /* @__PURE__ */ new Map();
|
|
460
|
+
/** Task ledger: task message id → record (status + ack timeline). */
|
|
461
|
+
taskLedger = /* @__PURE__ */ new Map();
|
|
458
462
|
gcTimer;
|
|
459
463
|
/** Stop the idle GC (call when the hub shuts down). */
|
|
460
464
|
dispose() {
|
|
@@ -625,15 +629,92 @@ var AgentHub = class {
|
|
|
625
629
|
send(from, to, kind, content) {
|
|
626
630
|
return this.route(from, to, kind, content);
|
|
627
631
|
}
|
|
628
|
-
/** Send a structured task message to `to` (or {@link BROADCAST}).
|
|
632
|
+
/** Send a structured task message to `to` (or {@link BROADCAST}). Records
|
|
633
|
+
* a ledger entry so the sender can later query status / wait for acks. */
|
|
629
634
|
sendTask(from, to, task) {
|
|
630
|
-
|
|
635
|
+
const message = this.route(from, to, "task", JSON.stringify(task));
|
|
636
|
+
this.recordTask(message, task);
|
|
637
|
+
return message;
|
|
631
638
|
}
|
|
632
|
-
/** Send an acknowledgement back to the sender of `ref`.
|
|
639
|
+
/** Send an acknowledgement back to the sender of `ref`. Updates the task
|
|
640
|
+
* ledger when `ref` is a known task; chat/notice refs still route. */
|
|
633
641
|
sendAck(from, ref, ack) {
|
|
634
|
-
const original = this.historyRing.findLast((
|
|
642
|
+
const original = this.historyRing.findLast((message2) => message2.id === ref);
|
|
635
643
|
if (!original) throw new Error(`cannot ack unknown message: ${ref}`);
|
|
636
|
-
|
|
644
|
+
const message = this.route(from, original.from, "ack", JSON.stringify(ack), ref);
|
|
645
|
+
this.applyAckToLedger(message, from, ref, ack);
|
|
646
|
+
return message;
|
|
647
|
+
}
|
|
648
|
+
/** Create or refresh the ledger row for a newly sent task. */
|
|
649
|
+
recordTask(message, task) {
|
|
650
|
+
const now = message.ts;
|
|
651
|
+
this.taskLedger.set(message.id, {
|
|
652
|
+
id: message.id,
|
|
653
|
+
from: message.from,
|
|
654
|
+
to: message.to,
|
|
655
|
+
prompt: task.prompt,
|
|
656
|
+
...task.context !== void 0 ? { context: task.context } : {},
|
|
657
|
+
...task.deliverable !== void 0 ? { deliverable: task.deliverable } : {},
|
|
658
|
+
status: "pending",
|
|
659
|
+
acks: [],
|
|
660
|
+
createdAt: now,
|
|
661
|
+
updatedAt: now
|
|
662
|
+
});
|
|
663
|
+
this.evictOverflowTasks();
|
|
664
|
+
}
|
|
665
|
+
/** Fold an ack into the task's timeline and status. Unknown refs are a
|
|
666
|
+
* no-op (chat/notice can be acked for routing without a ledger row). */
|
|
667
|
+
applyAckToLedger(message, from, ref, ack) {
|
|
668
|
+
const record = this.taskLedger.get(ref);
|
|
669
|
+
if (record === void 0) return;
|
|
670
|
+
record.acks.push({
|
|
671
|
+
from,
|
|
672
|
+
status: ack.status,
|
|
673
|
+
...ack.note !== void 0 ? { note: ack.note } : {},
|
|
674
|
+
ts: message.ts,
|
|
675
|
+
messageId: message.id
|
|
676
|
+
});
|
|
677
|
+
record.status = ack.status;
|
|
678
|
+
record.updatedAt = message.ts;
|
|
679
|
+
}
|
|
680
|
+
/** Keep the ledger bounded: drop oldest terminal tasks first. */
|
|
681
|
+
evictOverflowTasks() {
|
|
682
|
+
if (this.taskLedger.size <= MAX_TASKS) return;
|
|
683
|
+
const evictable = [...this.taskLedger.values()].filter((t) => TERMINAL_TASK_STATUSES.includes(t.status)).sort((a, b) => a.updatedAt - b.updatedAt);
|
|
684
|
+
let overflow = this.taskLedger.size - MAX_TASKS;
|
|
685
|
+
for (const record of evictable) {
|
|
686
|
+
if (overflow <= 0) break;
|
|
687
|
+
this.taskLedger.delete(record.id);
|
|
688
|
+
overflow--;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/** One task by message id (the ack `ref`). */
|
|
692
|
+
taskOf(ref) {
|
|
693
|
+
const record = this.taskLedger.get(ref);
|
|
694
|
+
return record === void 0 ? void 0 : { ...record, acks: record.acks.map((a) => ({ ...a })) };
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Tasks involving `peer`. `role`:
|
|
698
|
+
* - `sent` — tasks this peer delegated (`from`)
|
|
699
|
+
* - `received` — tasks addressed to this peer (or broadcast)
|
|
700
|
+
* - `all` (default) — either side
|
|
701
|
+
* Optional `status` filter matches the ledger's current status.
|
|
702
|
+
*/
|
|
703
|
+
listTasks(peerId, options) {
|
|
704
|
+
const role = options?.role ?? "all";
|
|
705
|
+
const limit = Math.max(1, options?.limit ?? 50);
|
|
706
|
+
const out = [];
|
|
707
|
+
for (const record of this.taskLedger.values()) {
|
|
708
|
+
const isSent = record.from === peerId;
|
|
709
|
+
const isReceived = record.to === peerId || record.to === BROADCAST;
|
|
710
|
+
if (role === "sent" && !isSent) continue;
|
|
711
|
+
if (role === "received" && !isReceived) continue;
|
|
712
|
+
if (role === "all" && !isSent && !isReceived) continue;
|
|
713
|
+
if (options?.status !== void 0 && record.status !== options.status) continue;
|
|
714
|
+
out.push({ ...record, acks: record.acks.map((a) => ({ ...a })) });
|
|
715
|
+
}
|
|
716
|
+
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
717
|
+
return out.slice(0, limit);
|
|
637
718
|
}
|
|
638
719
|
/** Recent messages involving `peer` (inbound and outbound, plus group
|
|
639
720
|
* channels the peer belongs to), newest first. */
|
|
@@ -856,11 +937,29 @@ var AgentHub = class {
|
|
|
856
937
|
* Long-poll for the next message addressed to `peer`: resolves immediately
|
|
857
938
|
* when a matching one is queued, otherwise waits up to `timeoutMs` (capped
|
|
858
939
|
* by `waitTimeoutMs`) or until `signal` aborts. `from` narrows to one sender.
|
|
940
|
+
* `ref` narrows to the ack of a specific task message id.
|
|
941
|
+
*
|
|
942
|
+
* Takes ONLY the first matching queued message. Calling poll() here would
|
|
943
|
+
* drain the whole mailbox and discard every message after the first —
|
|
944
|
+
* a multi-message burst (or a reconnect with a full queue) would silently
|
|
945
|
+
* lose messages that never reach wait/poll again.
|
|
859
946
|
*/
|
|
860
|
-
wait(peerId, timeoutMs, from, signal) {
|
|
947
|
+
wait(peerId, timeoutMs, from, signal, ref) {
|
|
861
948
|
const startedAt = Date.now();
|
|
862
|
-
const
|
|
863
|
-
|
|
949
|
+
const matches = (message) => {
|
|
950
|
+
if (from !== void 0 && message.from !== from) return false;
|
|
951
|
+
if (ref !== void 0 && !(message.kind === "ack" && message.ref === ref)) return false;
|
|
952
|
+
return true;
|
|
953
|
+
};
|
|
954
|
+
const queue = this.queues.get(peerId);
|
|
955
|
+
if (queue !== void 0 && queue.length > 0) {
|
|
956
|
+
const index = queue.findIndex(matches);
|
|
957
|
+
if (index >= 0) {
|
|
958
|
+
const [queued] = queue.splice(index, 1);
|
|
959
|
+
this.options.onMailboxesChanged?.();
|
|
960
|
+
return Promise.resolve({ type: "message", message: queued });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
864
963
|
const budget = Math.max(1, Math.min(Math.floor(timeoutMs), this.options.waitTimeoutMs));
|
|
865
964
|
return new Promise((resolve) => {
|
|
866
965
|
let settled = false;
|
|
@@ -889,7 +988,13 @@ var AgentHub = class {
|
|
|
889
988
|
}
|
|
890
989
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
891
990
|
const list = this.waiters.get(peerId) ?? [];
|
|
892
|
-
list.push({
|
|
991
|
+
list.push({
|
|
992
|
+
resolve: settle,
|
|
993
|
+
timer,
|
|
994
|
+
onAbort,
|
|
995
|
+
...from !== void 0 ? { from } : {},
|
|
996
|
+
...ref !== void 0 ? { ref } : {}
|
|
997
|
+
});
|
|
893
998
|
this.waiters.set(peerId, list);
|
|
894
999
|
});
|
|
895
1000
|
}
|
|
@@ -919,7 +1024,11 @@ var AgentHub = class {
|
|
|
919
1024
|
/** Queue or hand off a message; wake the first matching waiter for its target. */
|
|
920
1025
|
deliver(target, message) {
|
|
921
1026
|
const list = this.waiters.get(target) ?? [];
|
|
922
|
-
const index = list.findIndex((waiter) =>
|
|
1027
|
+
const index = list.findIndex((waiter) => {
|
|
1028
|
+
if (waiter.from !== void 0 && waiter.from !== message.from) return false;
|
|
1029
|
+
if (waiter.ref !== void 0 && !(message.kind === "ack" && message.ref === waiter.ref)) return false;
|
|
1030
|
+
return true;
|
|
1031
|
+
});
|
|
923
1032
|
if (index >= 0) {
|
|
924
1033
|
const [waiter] = list.splice(index, 1);
|
|
925
1034
|
this.waiters.set(target, list);
|
|
@@ -1362,7 +1471,7 @@ function hubTools(hub, registry, options) {
|
|
|
1362
1471
|
},
|
|
1363
1472
|
{
|
|
1364
1473
|
name: "bridge_task",
|
|
1365
|
-
description: "Delegate a structured task to another agent. The receiving agent decides whether to accept; expect an ack (accepted/rejected/done/failed) via bridge_wait / bridge_poll.",
|
|
1474
|
+
description: "Delegate a structured task to another agent. The receiving agent decides whether to accept; expect an ack (accepted/rejected/done/failed) via bridge_wait / bridge_poll. Track progress with bridge_task_status(ref) or bridge_tasks; wait specifically for the ack with bridge_wait({ ref }).",
|
|
1366
1475
|
inputSchema: schema(
|
|
1367
1476
|
{
|
|
1368
1477
|
to: str('Target peerId, or "all" to broadcast.'),
|
|
@@ -1380,7 +1489,7 @@ function hubTools(hub, registry, options) {
|
|
|
1380
1489
|
},
|
|
1381
1490
|
{
|
|
1382
1491
|
name: "bridge_ack",
|
|
1383
|
-
description: "Acknowledge a message received from another agent (usually a delegated task): accepted | rejected | done | failed. The ack is routed back to the original sender of `ref
|
|
1492
|
+
description: "Acknowledge a message received from another agent (usually a delegated task): accepted | rejected | done | failed. The ack is routed back to the original sender of `ref` and updates the hub task ledger (see bridge_task_status / bridge_tasks).",
|
|
1384
1493
|
inputSchema: schema(
|
|
1385
1494
|
{
|
|
1386
1495
|
ref: str("The id of the message being acknowledged."),
|
|
@@ -1398,14 +1507,61 @@ function hubTools(hub, registry, options) {
|
|
|
1398
1507
|
return receipt(hub.sendAck(peer, String(args.ref), { status, ...args.note !== void 0 ? { note: String(args.note) } : {} }));
|
|
1399
1508
|
})
|
|
1400
1509
|
},
|
|
1510
|
+
{
|
|
1511
|
+
name: "bridge_tasks",
|
|
1512
|
+
description: "List delegated tasks involving you, newest first. role=sent = tasks you assigned; role=received = tasks assigned to you (or broadcast); status filters the ledger state (pending/accepted/rejected/done/failed). Each row carries the full ack timeline.",
|
|
1513
|
+
inputSchema: schema({
|
|
1514
|
+
role: { type: "string", enum: ["sent", "received", "all"], description: "Which side of the task to list (default all)." },
|
|
1515
|
+
status: { type: "string", enum: ["pending", "accepted", "rejected", "done", "failed"], description: "Only tasks whose current ledger status matches." },
|
|
1516
|
+
limit: int("How many tasks to return (default 50).")
|
|
1517
|
+
}),
|
|
1518
|
+
handler: wrap(true, async (args, peer) => {
|
|
1519
|
+
const role = args.role === void 0 ? void 0 : String(args.role);
|
|
1520
|
+
if (role !== void 0 && role !== "sent" && role !== "received" && role !== "all") {
|
|
1521
|
+
throw new Error(`invalid role: ${role} (expected sent | received | all)`);
|
|
1522
|
+
}
|
|
1523
|
+
const status = args.status === void 0 ? void 0 : String(args.status);
|
|
1524
|
+
const validStatus = ["pending", "accepted", "rejected", "done", "failed"];
|
|
1525
|
+
if (status !== void 0 && !validStatus.includes(status)) {
|
|
1526
|
+
throw new Error(`invalid status: ${status} (expected ${validStatus.join(" | ")})`);
|
|
1527
|
+
}
|
|
1528
|
+
const tasks = hub.listTasks(peer, {
|
|
1529
|
+
...role !== void 0 ? { role } : {},
|
|
1530
|
+
...status !== void 0 ? { status } : {},
|
|
1531
|
+
...args.limit !== void 0 ? { limit: Number(args.limit) } : {}
|
|
1532
|
+
});
|
|
1533
|
+
return { tasks };
|
|
1534
|
+
})
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
name: "bridge_task_status",
|
|
1538
|
+
description: "Status of one delegated task: current ledger state (pending \u2192 accepted \u2192 done/failed, or rejected), the original prompt/context/deliverable, and every ack event (who, status, note, when). Use the task message id (the same id you pass as bridge_ack ref).",
|
|
1539
|
+
inputSchema: schema({ ref: str("Task message id (the id returned by bridge_task, also used as ack ref).") }, ["ref"]),
|
|
1540
|
+
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1541
|
+
const ref = String(args.ref);
|
|
1542
|
+
const task = hub.taskOf(ref);
|
|
1543
|
+
if (task === void 0) throw new Error(`unknown task: ${ref}`);
|
|
1544
|
+
if (task.from !== peer && task.to !== peer && task.to !== BROADCAST) {
|
|
1545
|
+
requireManager(peer, `reading task '${ref}'`, sessionId);
|
|
1546
|
+
}
|
|
1547
|
+
return { task };
|
|
1548
|
+
})
|
|
1549
|
+
},
|
|
1401
1550
|
{
|
|
1402
1551
|
name: "bridge_wait",
|
|
1403
|
-
description: "Wait (long-poll) for the next message addressed to you. Resolves immediately when one is queued; otherwise blocks until one arrives or the timeout fires. `from` narrows to one sender. Loop this tool to hold a real-time conversation.",
|
|
1552
|
+
description: "Wait (long-poll) for the next message addressed to you. Resolves immediately when one is queued; otherwise blocks until one arrives or the timeout fires. `from` narrows to one sender. `ref` waits specifically for the ack of that task id. Loop this tool to hold a real-time conversation.",
|
|
1404
1553
|
inputSchema: schema({
|
|
1405
1554
|
from: optStr("Only wait for messages from this peerId."),
|
|
1555
|
+
ref: optStr("Only wait for the ack of this task message id (bridge_task id)."),
|
|
1406
1556
|
timeoutMs: int(`Max wait in milliseconds (default ${DEFAULT_WAIT_MS}, ceiling is server waitTimeoutMs).`)
|
|
1407
1557
|
}),
|
|
1408
|
-
handler: wrap(true, async (args, peer) => presentWait(await hub.wait(
|
|
1558
|
+
handler: wrap(true, async (args, peer) => presentWait(await hub.wait(
|
|
1559
|
+
peer,
|
|
1560
|
+
args.timeoutMs === void 0 ? options.defaultWaitMs ?? DEFAULT_WAIT_MS : Number(args.timeoutMs),
|
|
1561
|
+
args.from === void 0 ? void 0 : String(args.from),
|
|
1562
|
+
void 0,
|
|
1563
|
+
args.ref === void 0 ? void 0 : String(args.ref)
|
|
1564
|
+
)))
|
|
1409
1565
|
},
|
|
1410
1566
|
{
|
|
1411
1567
|
name: "bridge_poll",
|
|
@@ -1428,7 +1584,11 @@ function hubTools(hub, registry, options) {
|
|
|
1428
1584
|
{
|
|
1429
1585
|
name: "bridge_history",
|
|
1430
1586
|
description: 'Recent messages involving you (newest first). Use to refresh context after a reconnect. Reading ANOTHER peer\'s conversation \u2014 or `peer: "all"` for the unfiltered tail \u2014 requires manager rights (hub managerPeers).',
|
|
1431
|
-
inputSchema: schema({
|
|
1587
|
+
inputSchema: schema({
|
|
1588
|
+
peer: optStr('PeerId whose conversation to inspect; "all" = every peer; default: yourself. Other peers / "all" require manager rights.'),
|
|
1589
|
+
channel: optStr("Read a group channel history instead of a peer conversation (you must be a member, or a manager)."),
|
|
1590
|
+
limit: int("How many messages to return (default 20).")
|
|
1591
|
+
}),
|
|
1432
1592
|
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1433
1593
|
const limit = Math.min(args.limit === void 0 ? 20 : Number(args.limit), 1e3);
|
|
1434
1594
|
const target = args.peer === void 0 ? peer : String(args.peer);
|
|
@@ -1736,6 +1896,7 @@ function adminPage() {
|
|
|
1736
1896
|
var SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1737
1897
|
var LATEST_VERSION = SUPPORTED_VERSIONS[0];
|
|
1738
1898
|
var MAX_BODY_BYTES = 1048576;
|
|
1899
|
+
var HEARTBEAT_MS = 2e4;
|
|
1739
1900
|
var SessionRegistry = class {
|
|
1740
1901
|
sessions = /* @__PURE__ */ new Set();
|
|
1741
1902
|
/** sessionId → peerId claimed via `bridge_register`. */
|
|
@@ -1881,7 +2042,7 @@ var SessionRegistry = class {
|
|
|
1881
2042
|
var McpStreamableHttpServer = class {
|
|
1882
2043
|
constructor(tools, info, registry, log = () => {
|
|
1883
2044
|
}, onInitialize = () => {
|
|
1884
|
-
}, authenticate, adminApi) {
|
|
2045
|
+
}, authenticate, adminApi, heartbeatMs) {
|
|
1885
2046
|
this.tools = tools;
|
|
1886
2047
|
this.info = info;
|
|
1887
2048
|
this.registry = registry;
|
|
@@ -1889,10 +2050,21 @@ var McpStreamableHttpServer = class {
|
|
|
1889
2050
|
this.onInitialize = onInitialize;
|
|
1890
2051
|
this.authenticate = authenticate;
|
|
1891
2052
|
this.adminApi = adminApi;
|
|
2053
|
+
this.heartbeatMs = heartbeatMs;
|
|
1892
2054
|
}
|
|
1893
2055
|
sseStreams = /* @__PURE__ */ new Map();
|
|
2056
|
+
heartbeatTimer;
|
|
1894
2057
|
/** Attach request handling for `path` (e.g. `/mcp`) to an http server. */
|
|
1895
2058
|
attach(server, path2) {
|
|
2059
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2060
|
+
if (this.sseStreams.size === 0) return;
|
|
2061
|
+
this.notifyAll("notifications/message", {
|
|
2062
|
+
level: "debug",
|
|
2063
|
+
logger: "bridge",
|
|
2064
|
+
data: { event: "heartbeat", ts: Date.now() }
|
|
2065
|
+
});
|
|
2066
|
+
}, this.heartbeatMs ?? HEARTBEAT_MS);
|
|
2067
|
+
this.heartbeatTimer.unref?.();
|
|
1896
2068
|
server.on("request", (req, res) => {
|
|
1897
2069
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1898
2070
|
if (url.pathname === ADMIN_PATH && req.method === "GET") {
|
|
@@ -1900,6 +2072,11 @@ var McpStreamableHttpServer = class {
|
|
|
1900
2072
|
res.end(adminPage());
|
|
1901
2073
|
return;
|
|
1902
2074
|
}
|
|
2075
|
+
if (url.pathname === "/healthz") {
|
|
2076
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", ...corsHeaders() });
|
|
2077
|
+
res.end(JSON.stringify({ ok: true, ts: Date.now() }));
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
1903
2080
|
if (url.pathname.startsWith(ADMIN_API_PATH) && this.adminApi !== void 0) {
|
|
1904
2081
|
void this.handleAdminApi(req, res, url);
|
|
1905
2082
|
return;
|
|
@@ -1925,6 +2102,7 @@ var McpStreamableHttpServer = class {
|
|
|
1925
2102
|
}
|
|
1926
2103
|
/** Close all open SSE streams (called on server shutdown). */
|
|
1927
2104
|
close() {
|
|
2105
|
+
if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
|
|
1928
2106
|
for (const stream of this.sseStreams.values()) stream.end();
|
|
1929
2107
|
this.sseStreams.clear();
|
|
1930
2108
|
}
|
|
@@ -2340,7 +2518,7 @@ var SQLiteStateStore = class {
|
|
|
2340
2518
|
|
|
2341
2519
|
// src/index.ts
|
|
2342
2520
|
var SERVER_NAME = "agent-comm-hub";
|
|
2343
|
-
var SERVER_VERSION = "0.
|
|
2521
|
+
var SERVER_VERSION = "0.8.0";
|
|
2344
2522
|
var DEFAULT_HOST = "127.0.0.1";
|
|
2345
2523
|
var DEFAULT_PORT = 18764;
|
|
2346
2524
|
var DEFAULT_PATH = "/mcp";
|
|
@@ -2358,9 +2536,10 @@ var DEFAULT_CONFIG = {
|
|
|
2358
2536
|
connectedWindowMs: 3e4,
|
|
2359
2537
|
peerIdleTimeoutMs: 6e5,
|
|
2360
2538
|
// The companion desktop app (and `agent-comm-hub status` probe) registers
|
|
2361
|
-
// as `agent-hub-cli`;
|
|
2362
|
-
// are
|
|
2363
|
-
|
|
2539
|
+
// as `agent-hub-cli`; the web admin console registers as `hub-admin`.
|
|
2540
|
+
// Both are roster managers. Agents themselves are NOT managers — they
|
|
2541
|
+
// keep chatting, a GUI manages.
|
|
2542
|
+
managerPeers: ["agent-hub-cli", "hub-admin"]
|
|
2364
2543
|
};
|
|
2365
2544
|
function authTokensAdmin(file, reloadAuth) {
|
|
2366
2545
|
if (file === void 0) return void 0;
|
|
@@ -2610,7 +2789,8 @@ function startHub(config = {}, log = console) {
|
|
|
2610
2789
|
}
|
|
2611
2790
|
},
|
|
2612
2791
|
auth,
|
|
2613
|
-
authTokensAdmin(resolved.authTokens, authReload)
|
|
2792
|
+
authTokensAdmin(resolved.authTokens, authReload),
|
|
2793
|
+
resolved.heartbeatMs
|
|
2614
2794
|
);
|
|
2615
2795
|
const server = createServer();
|
|
2616
2796
|
mcp.attach(server, resolved.path);
|
|
@@ -2660,6 +2840,7 @@ export {
|
|
|
2660
2840
|
SERVER_VERSION,
|
|
2661
2841
|
SQLiteStateStore,
|
|
2662
2842
|
SessionRegistry,
|
|
2843
|
+
TERMINAL_TASK_STATUSES,
|
|
2663
2844
|
addToken,
|
|
2664
2845
|
autoRegisterPeer,
|
|
2665
2846
|
decodeContent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-comm-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Generic multi-peer MCP hub: any MCP-capable agent (MiniMax Code, Claude Code, opencode, Codex, Gemini CLI, DSH, ...) connects to one local streamable-http endpoint and they chat, delegate tasks, and acknowledge in real time",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=lib/cli.js && esbuild src/index.ts --bundle --platform=node --format=esm --outfile=lib/index.js && esbuild src/setup.ts --bundle --platform=node --format=esm --outfile=lib/setup.js",
|
|
39
39
|
"build:test": "esbuild test/entry.ts --bundle --platform=node --format=esm --outfile=test/entry.mjs && esbuild test/setup-entry.ts --bundle --platform=node --format=esm --outfile=test/setup-entry.mjs && esbuild test/ops-entry.ts --bundle --platform=node --format=esm --outfile=test/ops-entry.mjs && esbuild test/herdr-entry.ts --bundle --platform=node --format=esm --outfile=test/herdr-entry.mjs && esbuild test/discover-entry.ts --bundle --platform=node --format=esm --outfile=test/discover-entry.mjs",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"test": "pnpm run build:test && node test/smoke.mjs && node test/setup.mjs && node test/ops.mjs && node test/herdr.mjs && node test/discover.mjs",
|
|
41
|
+
"test": "pnpm run build:test && pnpm run build && node test/smoke.mjs && node test/setup.mjs && node test/ops.mjs && node test/herdr.mjs && node test/discover.mjs && node test/e2e.mjs",
|
|
42
42
|
"pack": "pnpm run build && pnpm pack"
|
|
43
43
|
}
|
|
44
44
|
}
|