agent-comm-hub 0.3.0 → 0.5.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 +43 -6
- package/README.zh.md +42 -5
- package/agents/SKILL.md +3 -1
- package/agents/registry.json +201 -0
- package/assets/ach-cn.png +0 -0
- package/assets/ach-en.png +0 -0
- package/lib/cli.js +347 -98
- package/lib/index.js +18 -5
- package/lib/setup.js +219 -69
- package/package.json +44 -46
package/lib/cli.js
CHANGED
|
@@ -512,6 +512,12 @@ var AgentHub = class {
|
|
|
512
512
|
const filtered = this.historyRing.filter((message) => message.from === peerId || message.to === peerId || message.to === BROADCAST);
|
|
513
513
|
return filtered.slice(-Math.max(0, limit)).reverse();
|
|
514
514
|
}
|
|
515
|
+
/** Most recent messages across every peer, unfiltered (newest first).
|
|
516
|
+
* Backs `bridge_history { peer: "all" }` — lets an archiver (the desktop
|
|
517
|
+
* app) capture peer-to-peer traffic it is not a party of. */
|
|
518
|
+
historyAll(limit) {
|
|
519
|
+
return this.historyRing.slice(-Math.max(0, limit)).reverse();
|
|
520
|
+
}
|
|
515
521
|
/**
|
|
516
522
|
* Live summary for the status tool. `livePeers` (sessions with a live SSE
|
|
517
523
|
* stream) count as connected even without recent tool activity.
|
|
@@ -840,9 +846,13 @@ function hubTools(hub, registry, options) {
|
|
|
840
846
|
},
|
|
841
847
|
{
|
|
842
848
|
name: "bridge_history",
|
|
843
|
-
description:
|
|
844
|
-
inputSchema: schema({ peer: optStr(
|
|
845
|
-
handler: wrap(true, async (args, peer) =>
|
|
849
|
+
description: 'Recent messages involving you (newest first); pass `peer` to inspect another peer\'s conversation, or `peer: "all"` for the unfiltered tail across every peer. Use to refresh context after a reconnect.',
|
|
850
|
+
inputSchema: schema({ peer: optStr('PeerId whose conversation to inspect; "all" = every peer; default: yourself.'), limit: int("How many messages to return (default 20).") }),
|
|
851
|
+
handler: wrap(true, async (args, peer) => {
|
|
852
|
+
const limit = Math.min(args.limit === void 0 ? 20 : Number(args.limit), 1e3);
|
|
853
|
+
const messages = args.peer === "all" ? hub.historyAll(limit) : hub.history(args.peer === void 0 ? peer : String(args.peer), limit);
|
|
854
|
+
return { messages: messages.map(present) };
|
|
855
|
+
})
|
|
846
856
|
},
|
|
847
857
|
// ---- herdr control tools ------------------------------------------
|
|
848
858
|
// These type into real agent terminals via the herdr runtime. They are
|
|
@@ -1334,7 +1344,7 @@ function readBody(req) {
|
|
|
1334
1344
|
|
|
1335
1345
|
// src/index.ts
|
|
1336
1346
|
var SERVER_NAME = "agent-comm-hub";
|
|
1337
|
-
var SERVER_VERSION = "0.
|
|
1347
|
+
var SERVER_VERSION = "0.5.0";
|
|
1338
1348
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1339
1349
|
var DEFAULT_PORT = 18764;
|
|
1340
1350
|
var DEFAULT_PATH = "/mcp";
|
|
@@ -1343,7 +1353,10 @@ var DEFAULT_CONFIG = {
|
|
|
1343
1353
|
port: DEFAULT_PORT,
|
|
1344
1354
|
path: DEFAULT_PATH,
|
|
1345
1355
|
maxQueue: 200,
|
|
1346
|
-
|
|
1356
|
+
// 100 was too small to survive a long multi-agent session: the ring is the
|
|
1357
|
+
// only archive source until the desktop app persists it to SQLite, and a
|
|
1358
|
+
// night of agent-to-agent chatter evicts everything within minutes.
|
|
1359
|
+
historyLimit: 1e3,
|
|
1347
1360
|
waitTimeoutMs: 6e4,
|
|
1348
1361
|
defaultWaitMs: 3e4,
|
|
1349
1362
|
connectedWindowMs: 3e4,
|
|
@@ -1410,15 +1423,190 @@ function startHub(config = {}, log2 = console) {
|
|
|
1410
1423
|
}
|
|
1411
1424
|
|
|
1412
1425
|
// src/setup.ts
|
|
1413
|
-
import { copyFile, mkdir, readFile,
|
|
1414
|
-
import { existsSync } from "node:fs";
|
|
1426
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1427
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1428
|
+
import { homedir as homedir2 } from "node:os";
|
|
1429
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1430
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1431
|
+
|
|
1432
|
+
// src/discover.ts
|
|
1433
|
+
import { execFileSync } from "node:child_process";
|
|
1434
|
+
import { existsSync, readdirSync, accessSync, readFileSync, constants as fsConstants } from "node:fs";
|
|
1415
1435
|
import { homedir } from "node:os";
|
|
1416
|
-
import { dirname, join } from "node:path";
|
|
1436
|
+
import { dirname, join, sep } from "node:path";
|
|
1417
1437
|
import { fileURLToPath } from "node:url";
|
|
1438
|
+
function registryFile() {
|
|
1439
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "agents", "registry.json");
|
|
1440
|
+
}
|
|
1441
|
+
function expandHome(file, home) {
|
|
1442
|
+
return file.startsWith("~/") ? join(home, file.slice(2)) : file;
|
|
1443
|
+
}
|
|
1444
|
+
function expandConfigFile(file, home) {
|
|
1445
|
+
const expanded = expandHome(file, home);
|
|
1446
|
+
const star = expanded.indexOf("*");
|
|
1447
|
+
if (star < 0) return [expanded];
|
|
1448
|
+
const prefix = expanded.slice(0, star);
|
|
1449
|
+
const suffix = expanded.slice(star + 1);
|
|
1450
|
+
const base = prefix.slice(0, prefix.lastIndexOf(sep));
|
|
1451
|
+
if (!existsSync(base)) return [];
|
|
1452
|
+
return readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(base, entry.name, suffix));
|
|
1453
|
+
}
|
|
1454
|
+
function validateRegistry(registry) {
|
|
1455
|
+
if (!Array.isArray(registry.agents)) throw new Error('registry: missing "agents" array');
|
|
1456
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1457
|
+
for (const agent of registry.agents) {
|
|
1458
|
+
if (typeof agent.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(agent.id)) {
|
|
1459
|
+
throw new Error(`registry: bad agent id ${JSON.stringify(agent.id)}`);
|
|
1460
|
+
}
|
|
1461
|
+
if (ids.has(agent.id)) throw new Error(`registry: duplicate agent id '${agent.id}'`);
|
|
1462
|
+
ids.add(agent.id);
|
|
1463
|
+
if (!Array.isArray(agent.probe)) throw new Error(`registry: ${agent.id}: probe must be an array`);
|
|
1464
|
+
if (agent.npm !== void 0 && !Array.isArray(agent.npm)) throw new Error(`registry: ${agent.id}: npm must be an array`);
|
|
1465
|
+
if (!Array.isArray(agent.configs)) throw new Error(`registry: ${agent.id}: configs must be an array`);
|
|
1466
|
+
for (const config of agent.configs) {
|
|
1467
|
+
if (typeof config.file !== "string" || !config.file.startsWith("~/")) {
|
|
1468
|
+
throw new Error(`registry: ${agent.id}: config file must be '~'-relative`);
|
|
1469
|
+
}
|
|
1470
|
+
if (config.file.split("/").includes("..")) {
|
|
1471
|
+
throw new Error(`registry: ${agent.id}: config file must not contain '..'`);
|
|
1472
|
+
}
|
|
1473
|
+
if (config.file.split("*").length > 2) {
|
|
1474
|
+
throw new Error(`registry: ${agent.id}: at most one '*' segment allowed`);
|
|
1475
|
+
}
|
|
1476
|
+
if (!["json", "toml", "dsh"].includes(config.strategy)) {
|
|
1477
|
+
throw new Error(`registry: ${agent.id}: unknown strategy '${config.strategy}'`);
|
|
1478
|
+
}
|
|
1479
|
+
if (config.strategy === "json" && (typeof config.section !== "string" || config.entry === null)) {
|
|
1480
|
+
throw new Error(`registry: ${agent.id}: json strategy needs a section and an entry`);
|
|
1481
|
+
}
|
|
1482
|
+
if (config.strategy !== "json" && config.entry !== null) {
|
|
1483
|
+
throw new Error(`registry: ${agent.id}: only json strategy may carry an entry`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (agent.skill !== null && (typeof agent.skill !== "string" || !agent.skill.startsWith("~/"))) {
|
|
1487
|
+
throw new Error(`registry: ${agent.id}: skill must be '~'-relative or null`);
|
|
1488
|
+
}
|
|
1489
|
+
if (agent.os !== void 0 && (!Array.isArray(agent.os) || agent.os.some((os2) => !["win32", "darwin", "linux"].includes(os2)))) {
|
|
1490
|
+
throw new Error(`registry: ${agent.id}: invalid os list`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
function loadRegistry(file = registryFile()) {
|
|
1495
|
+
const registry = JSON.parse(readFileSync(file, "utf8"));
|
|
1496
|
+
validateRegistry(registry);
|
|
1497
|
+
return registry;
|
|
1498
|
+
}
|
|
1499
|
+
function commandOnPath(command, pathEnv, pathext, platform) {
|
|
1500
|
+
const dirs = pathEnv.split(platform === "win32" ? ";" : ":");
|
|
1501
|
+
const extensions = platform === "win32" ? ["", ...(pathext || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
1502
|
+
for (const dir of dirs) {
|
|
1503
|
+
const base = dir === "" ? "." : dir;
|
|
1504
|
+
for (const ext of extensions) {
|
|
1505
|
+
const candidate = join(base, command + ext);
|
|
1506
|
+
try {
|
|
1507
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
1508
|
+
return true;
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
function readNpmNames(root) {
|
|
1516
|
+
const names = [];
|
|
1517
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
1518
|
+
if (!entry.isDirectory()) continue;
|
|
1519
|
+
if (entry.name.startsWith("@")) {
|
|
1520
|
+
const scopeDir = join(root, entry.name);
|
|
1521
|
+
for (const sub of readdirSync(scopeDir, { withFileTypes: true })) {
|
|
1522
|
+
if (sub.isDirectory()) names.push(sub.name);
|
|
1523
|
+
}
|
|
1524
|
+
} else {
|
|
1525
|
+
names.push(entry.name);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return names;
|
|
1529
|
+
}
|
|
1530
|
+
function npmGlobalRoot(platform) {
|
|
1531
|
+
try {
|
|
1532
|
+
const out = execFileSync(platform === "win32" ? "npm.cmd" : "npm", ["root", "-g"], { encoding: "utf8", windowsHide: true }).trim();
|
|
1533
|
+
return out || null;
|
|
1534
|
+
} catch {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function npmFallbackRoots(home, platform) {
|
|
1539
|
+
if (platform === "win32") {
|
|
1540
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
1541
|
+
return [join(appData, "npm", "node_modules")];
|
|
1542
|
+
}
|
|
1543
|
+
return [
|
|
1544
|
+
"/usr/local/lib/node_modules",
|
|
1545
|
+
"/usr/lib/node_modules",
|
|
1546
|
+
...existsSync(join(home, ".nvm", "versions", "node")) ? readdirSync(join(home, ".nvm", "versions", "node")).map((dir) => join(home, ".nvm", "versions", "node", dir, "lib", "node_modules")) : []
|
|
1547
|
+
];
|
|
1548
|
+
}
|
|
1549
|
+
function discover(registry, options = {}) {
|
|
1550
|
+
const home = options.homeDir ?? homedir();
|
|
1551
|
+
const platform = options.platform ?? process.platform;
|
|
1552
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
1553
|
+
const pathext = options.pathext ?? process.env.PATHEXT ?? "";
|
|
1554
|
+
let npmNames = null;
|
|
1555
|
+
if (!options.noNpm) {
|
|
1556
|
+
if (options.npmRoot !== void 0 && options.npmRoot !== null) {
|
|
1557
|
+
npmNames = existsSync(options.npmRoot) ? readNpmNames(options.npmRoot) : [];
|
|
1558
|
+
} else if (options.npmRoot !== null) {
|
|
1559
|
+
const root = npmGlobalRoot(platform);
|
|
1560
|
+
if (root && existsSync(root)) {
|
|
1561
|
+
npmNames = readNpmNames(root);
|
|
1562
|
+
} else {
|
|
1563
|
+
npmNames = [];
|
|
1564
|
+
for (const fallback of npmFallbackRoots(home, platform)) {
|
|
1565
|
+
if (existsSync(fallback)) {
|
|
1566
|
+
npmNames = readNpmNames(fallback);
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return registry.agents.filter((agent) => agent.os === void 0 || agent.os.includes(platform)).map((agent) => {
|
|
1574
|
+
let source = "none";
|
|
1575
|
+
const configFiles = [];
|
|
1576
|
+
for (const config of agent.configs) {
|
|
1577
|
+
for (const file of expandConfigFile(config.file, home)) {
|
|
1578
|
+
if (existsSync(file)) {
|
|
1579
|
+
configFiles.push(file);
|
|
1580
|
+
if (source === "none") source = "config";
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (source === "none" && agent.probe.some((command) => commandOnPath(command, pathEnv, pathext, platform))) {
|
|
1585
|
+
source = "path";
|
|
1586
|
+
}
|
|
1587
|
+
if (source === "none" && npmNames !== null && (agent.probe.some((command) => npmNames.includes(command)) || (agent.npm ?? []).some((name) => npmNames.includes(name)))) {
|
|
1588
|
+
source = "npm";
|
|
1589
|
+
}
|
|
1590
|
+
return { id: agent.id, source, configFiles, present: source !== "none" };
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function runDiscover(options = {}) {
|
|
1594
|
+
const log2 = options.log ?? ((message) => console.log(message));
|
|
1595
|
+
const found = discover(loadRegistry(), options);
|
|
1596
|
+
log2("discovered agents:");
|
|
1597
|
+
for (const agent of found) {
|
|
1598
|
+
const status = agent.present ? agent.source : "not installed";
|
|
1599
|
+
const files = agent.configFiles.length > 0 ? ` \u2014 ${agent.configFiles.join(", ")}` : "";
|
|
1600
|
+
log2(` ${agent.id.padEnd(16)} ${status}${files}`);
|
|
1601
|
+
}
|
|
1602
|
+
return found;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/setup.ts
|
|
1418
1606
|
var DEFAULT_URL = "http://127.0.0.1:18764/mcp";
|
|
1419
1607
|
var DEFAULT_SERVER = "agent-hub";
|
|
1420
1608
|
function defaultSkillSrc() {
|
|
1421
|
-
return
|
|
1609
|
+
return join2(dirname2(fileURLToPath2(import.meta.url)), "..", "agents", "SKILL.md");
|
|
1422
1610
|
}
|
|
1423
1611
|
function stamp() {
|
|
1424
1612
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1426,7 +1614,7 @@ function stamp() {
|
|
|
1426
1614
|
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1427
1615
|
}
|
|
1428
1616
|
async function readJson(file) {
|
|
1429
|
-
if (!
|
|
1617
|
+
if (!existsSync2(file)) return null;
|
|
1430
1618
|
try {
|
|
1431
1619
|
return JSON.parse(await readFile(file, "utf8"));
|
|
1432
1620
|
} catch (error) {
|
|
@@ -1434,7 +1622,7 @@ async function readJson(file) {
|
|
|
1434
1622
|
}
|
|
1435
1623
|
}
|
|
1436
1624
|
async function writeJsonNoBom(file, doc) {
|
|
1437
|
-
await mkdir(
|
|
1625
|
+
await mkdir(dirname2(file), { recursive: true });
|
|
1438
1626
|
await writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
1439
1627
|
}
|
|
1440
1628
|
async function backup(file) {
|
|
@@ -1455,7 +1643,7 @@ function resolveSection(doc, section) {
|
|
|
1455
1643
|
return node;
|
|
1456
1644
|
}
|
|
1457
1645
|
async function mergeJsonServer(file, section, entry, opts) {
|
|
1458
|
-
if (!
|
|
1646
|
+
if (!existsSync2(file)) return "skipped";
|
|
1459
1647
|
const doc = await readJson(file);
|
|
1460
1648
|
if (doc === null) return "skipped";
|
|
1461
1649
|
const servers = resolveSection(doc, section);
|
|
@@ -1477,7 +1665,7 @@ async function mergeJsonServer(file, section, entry, opts) {
|
|
|
1477
1665
|
return "changed";
|
|
1478
1666
|
}
|
|
1479
1667
|
async function mergeTomlSection(file, opts) {
|
|
1480
|
-
if (!
|
|
1668
|
+
if (!existsSync2(file)) return "skipped";
|
|
1481
1669
|
const text = await readFile(file, "utf8");
|
|
1482
1670
|
const marker = `[mcp_servers.${opts.serverName}]`;
|
|
1483
1671
|
const markerRe = new RegExp(`^\\[mcp_servers\\.${escapeRegExp(opts.serverName)}\\]`, "m");
|
|
@@ -1515,7 +1703,7 @@ ${DSH_PATCH_MARKER} (installed by \`agent-comm-hub setup\`; undo with \`setup --
|
|
|
1515
1703
|
`;
|
|
1516
1704
|
}
|
|
1517
1705
|
async function mergeDshPatch(file, opts) {
|
|
1518
|
-
if (!
|
|
1706
|
+
if (!existsSync2(file)) return "skipped";
|
|
1519
1707
|
const text = await readFile(file, "utf8");
|
|
1520
1708
|
const lines = text.split("\n");
|
|
1521
1709
|
const markerLine = lines.findIndex((line) => line.includes(DSH_PATCH_MARKER));
|
|
@@ -1565,29 +1753,40 @@ async function mergeDshPatch(file, opts) {
|
|
|
1565
1753
|
}
|
|
1566
1754
|
async function syncSkill(skillDir, skillSrc, remove, log2) {
|
|
1567
1755
|
if (remove) {
|
|
1568
|
-
if (
|
|
1569
|
-
await mkdir(
|
|
1756
|
+
if (existsSync2(skillDir)) {
|
|
1757
|
+
await mkdir(dirname2(skillDir), { recursive: true });
|
|
1570
1758
|
await rmRecursive(skillDir);
|
|
1571
1759
|
log2(` skill removed: ${skillDir}`);
|
|
1572
1760
|
}
|
|
1573
1761
|
return;
|
|
1574
1762
|
}
|
|
1575
|
-
if (!
|
|
1763
|
+
if (!existsSync2(skillSrc)) {
|
|
1576
1764
|
log2(` SKILL.md source missing: ${skillSrc} (skipped)`);
|
|
1577
1765
|
return;
|
|
1578
1766
|
}
|
|
1579
1767
|
await mkdir(skillDir, { recursive: true });
|
|
1580
|
-
await copyFile(skillSrc,
|
|
1581
|
-
log2(` skill -> ${
|
|
1768
|
+
await copyFile(skillSrc, join2(skillDir, "SKILL.md"));
|
|
1769
|
+
log2(` skill -> ${join2(skillDir, "SKILL.md")}`);
|
|
1582
1770
|
}
|
|
1583
1771
|
async function rmRecursive(dir) {
|
|
1584
1772
|
const { rm } = await import("node:fs/promises");
|
|
1585
1773
|
await rm(dir, { recursive: true, force: true });
|
|
1586
1774
|
}
|
|
1775
|
+
function substitute(entry, values) {
|
|
1776
|
+
const out = {};
|
|
1777
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
1778
|
+
if (typeof value === "string") {
|
|
1779
|
+
out[key] = value.replaceAll("{url}", values.url).replaceAll("{serverName}", values.serverName);
|
|
1780
|
+
} else {
|
|
1781
|
+
out[key] = value;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return out;
|
|
1785
|
+
}
|
|
1587
1786
|
async function runSetup(options = {}) {
|
|
1588
1787
|
const url = options.url ?? DEFAULT_URL;
|
|
1589
1788
|
const serverName = options.serverName ?? DEFAULT_SERVER;
|
|
1590
|
-
const home = options.homeDir ??
|
|
1789
|
+
const home = options.homeDir ?? homedir2();
|
|
1591
1790
|
const skillSrc = options.skillSrc ?? defaultSkillSrc();
|
|
1592
1791
|
const remove = options.remove === true;
|
|
1593
1792
|
const log2 = options.log ?? ((message) => console.log(message));
|
|
@@ -1597,65 +1796,40 @@ async function runSetup(options = {}) {
|
|
|
1597
1796
|
else if (status === "unchanged" || status === "absent") summary.unchanged.push(`${label}: ${file}`);
|
|
1598
1797
|
else if (status === "skipped") summary.skipped.push(`${label}: ${file}`);
|
|
1599
1798
|
};
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
record(status, target.label, target.file);
|
|
1612
|
-
} catch (error) {
|
|
1613
|
-
summary.errors.push(`${target.label}: ${target.file} \u2014 ${error.message}`);
|
|
1614
|
-
log2(` ${target.label}: SKIPPED \u2014 ${error.message}`);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
const codexFile = join(home, ".codex", "config.toml");
|
|
1618
|
-
try {
|
|
1619
|
-
const status = await mergeTomlSection(codexFile, { serverName, url, remove });
|
|
1620
|
-
record(status, "codex", codexFile);
|
|
1621
|
-
} catch (error) {
|
|
1622
|
-
summary.errors.push(`codex: ${codexFile} \u2014 ${error.message}`);
|
|
1623
|
-
log2(` codex: SKIPPED \u2014 ${error.message}`);
|
|
1624
|
-
}
|
|
1625
|
-
const dshProfilesDir = join(home, ".dsh", "profiles");
|
|
1626
|
-
if (existsSync(dshProfilesDir)) {
|
|
1627
|
-
let entries = [];
|
|
1628
|
-
try {
|
|
1629
|
-
entries = await readdir(dshProfilesDir, { withFileTypes: true });
|
|
1630
|
-
} catch (error) {
|
|
1631
|
-
summary.errors.push(`dsh profiles scan \u2014 ${error.message}`);
|
|
1799
|
+
const registry = loadRegistry();
|
|
1800
|
+
const found = discover(registry, { homeDir: home, pathEnv: options.pathEnv, noNpm: options.noNpm === true });
|
|
1801
|
+
const only = options.agent;
|
|
1802
|
+
let targetAgents = [];
|
|
1803
|
+
if (only !== void 0) {
|
|
1804
|
+
const match = registry.agents.find((agent) => agent.id === only);
|
|
1805
|
+
if (match === void 0) {
|
|
1806
|
+
log2(`agent '${only}' is not in the registry (see agents/registry.json)`);
|
|
1807
|
+
} else {
|
|
1808
|
+
targetAgents = [match];
|
|
1809
|
+
log2(`configure only: ${only}`);
|
|
1632
1810
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1811
|
+
} else {
|
|
1812
|
+
targetAgents = found.filter((agent) => agent.present).map((agent) => registry.agents.find((entry) => entry.id === agent.id));
|
|
1813
|
+
const present2 = targetAgents.map((agent) => agent.id);
|
|
1814
|
+
log2(`discovered: ${present2.length > 0 ? present2.join(", ") : "none"}`);
|
|
1815
|
+
}
|
|
1816
|
+
for (const agent of targetAgents) {
|
|
1817
|
+
for (const config of agent.configs) {
|
|
1818
|
+
for (const file of expandConfigFile(config.file, home)) {
|
|
1819
|
+
try {
|
|
1820
|
+
const status = config.strategy === "json" ? await mergeJsonServer(file, config.section, substitute(config.entry, { url, serverName }), { serverName, url, remove }) : config.strategy === "toml" ? await mergeTomlSection(file, { serverName, url, remove }) : await mergeDshPatch(file, { serverName, url, remove });
|
|
1821
|
+
record(status, agent.id, file);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
summary.errors.push(`${agent.id}: ${file} \u2014 ${error.message}`);
|
|
1824
|
+
log2(` ${agent.id}: SKIPPED \u2014 ${error.message}`);
|
|
1825
|
+
}
|
|
1642
1826
|
}
|
|
1643
1827
|
}
|
|
1644
1828
|
}
|
|
1645
|
-
const skillDirs = [
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
join(home, ".config", "opencode", "skills", serverName),
|
|
1650
|
-
join(home, ".kimi-code", "skills", serverName),
|
|
1651
|
-
join(home, ".gemini", "skills", serverName),
|
|
1652
|
-
join(home, ".codex", "skills", serverName),
|
|
1653
|
-
join(home, ".zcode", "skills", serverName),
|
|
1654
|
-
join(home, ".claude", "skills", serverName),
|
|
1655
|
-
// config is manual; skill still useful
|
|
1656
|
-
join(home, ".dsh", "skills", serverName)
|
|
1657
|
-
// DSH skill (config auto-installed)
|
|
1658
|
-
];
|
|
1829
|
+
const skillDirs = [join2(home, ".agents", "skills", serverName)];
|
|
1830
|
+
for (const agent of targetAgents) {
|
|
1831
|
+
if (agent.skill !== null) skillDirs.push(join2(expandHome(agent.skill, home), serverName));
|
|
1832
|
+
}
|
|
1659
1833
|
for (const dir of skillDirs) {
|
|
1660
1834
|
try {
|
|
1661
1835
|
await syncSkill(dir, skillSrc, remove, log2);
|
|
@@ -1670,12 +1844,12 @@ async function runSetup(options = {}) {
|
|
|
1670
1844
|
}
|
|
1671
1845
|
|
|
1672
1846
|
// src/ops.ts
|
|
1673
|
-
import { execFileSync } from "node:child_process";
|
|
1847
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1674
1848
|
import { request } from "node:http";
|
|
1675
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1676
|
-
import { homedir as
|
|
1677
|
-
import { dirname as
|
|
1678
|
-
import { fileURLToPath as
|
|
1849
|
+
import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
1850
|
+
import { homedir as homedir3 } from "node:os";
|
|
1851
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
1852
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1679
1853
|
async function runStatus(options = {}) {
|
|
1680
1854
|
const host = options.host ?? "127.0.0.1";
|
|
1681
1855
|
const port = options.port ?? 18764;
|
|
@@ -1727,8 +1901,8 @@ async function runStatus(options = {}) {
|
|
|
1727
1901
|
function runUpdate() {
|
|
1728
1902
|
const messages = [];
|
|
1729
1903
|
try {
|
|
1730
|
-
const pkgFile =
|
|
1731
|
-
const before = JSON.parse(
|
|
1904
|
+
const pkgFile = join3(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
1905
|
+
const before = JSON.parse(readFileSync2(pkgFile, "utf8")).version ?? "?";
|
|
1732
1906
|
messages.push(`current version: ${before}`);
|
|
1733
1907
|
const script = [
|
|
1734
1908
|
"import { execFileSync } from 'node:child_process'",
|
|
@@ -1744,7 +1918,7 @@ function runUpdate() {
|
|
|
1744
1918
|
"if (after === before) console.log('already up to date (v' + after + ')')",
|
|
1745
1919
|
"else console.log('updated: v' + before + ' -> v' + after)"
|
|
1746
1920
|
].join("\n");
|
|
1747
|
-
const out =
|
|
1921
|
+
const out = execFileSync2(process.execPath, ["--input-type=module", "-e", script], { encoding: "utf8", windowsHide: true });
|
|
1748
1922
|
messages.push(out.trim());
|
|
1749
1923
|
messages.push("restart the hub (agent-comm-hub) to pick up the new version");
|
|
1750
1924
|
return { ok: true, messages };
|
|
@@ -1753,14 +1927,14 @@ function runUpdate() {
|
|
|
1753
1927
|
}
|
|
1754
1928
|
}
|
|
1755
1929
|
function cliPath() {
|
|
1756
|
-
return
|
|
1930
|
+
return fileURLToPath3(import.meta.url);
|
|
1757
1931
|
}
|
|
1758
1932
|
function nodeExe() {
|
|
1759
1933
|
return process.execPath;
|
|
1760
1934
|
}
|
|
1761
1935
|
function run(command, args, dryRun) {
|
|
1762
1936
|
if (dryRun) return `[dry-run] ${command} ${args.join(" ")}`;
|
|
1763
|
-
return
|
|
1937
|
+
return execFileSync2(command, args, { encoding: "utf8", windowsHide: true }).trim();
|
|
1764
1938
|
}
|
|
1765
1939
|
function runService(options) {
|
|
1766
1940
|
const messages = [];
|
|
@@ -1768,11 +1942,12 @@ function runService(options) {
|
|
|
1768
1942
|
const host = options.host ?? "127.0.0.1";
|
|
1769
1943
|
const path2 = options.path ?? "/mcp";
|
|
1770
1944
|
const dryRun = options.dryRun === true;
|
|
1945
|
+
const platform = options.platform ?? process.platform;
|
|
1771
1946
|
try {
|
|
1772
|
-
if (
|
|
1773
|
-
const appData = process.env.APPDATA ??
|
|
1774
|
-
const launcherDir =
|
|
1775
|
-
const vbs =
|
|
1947
|
+
if (platform === "win32") {
|
|
1948
|
+
const appData = process.env.APPDATA ?? join3(homedir3(), "AppData", "Roaming");
|
|
1949
|
+
const launcherDir = join3(appData, "agent-comm-hub");
|
|
1950
|
+
const vbs = join3(launcherDir, "agent-comm-hub.vbs");
|
|
1776
1951
|
const runKey = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
1777
1952
|
const valueName = "agent-comm-hub";
|
|
1778
1953
|
if (options.action === "install") {
|
|
@@ -1785,7 +1960,7 @@ function runService(options) {
|
|
|
1785
1960
|
} else {
|
|
1786
1961
|
mkdirSync(launcherDir, { recursive: true });
|
|
1787
1962
|
writeFileSync(vbs, vbsContent);
|
|
1788
|
-
|
|
1963
|
+
execFileSync2("reg", ["add", runKey, "/v", valueName, "/t", "REG_SZ", "/d", `wscript.exe "${vbs}"`, "/f"], { encoding: "utf8", windowsHide: true });
|
|
1789
1964
|
messages.push(`auto-start registered: HKCU Run '${valueName}' -> hidden wscript launcher "${vbs}"`);
|
|
1790
1965
|
messages.push(`start it now with: wscript.exe "${vbs}"`);
|
|
1791
1966
|
}
|
|
@@ -1795,7 +1970,7 @@ function runService(options) {
|
|
|
1795
1970
|
messages.push(`[dry-run] del ${vbs}`);
|
|
1796
1971
|
} else {
|
|
1797
1972
|
try {
|
|
1798
|
-
|
|
1973
|
+
execFileSync2("reg", ["delete", runKey, "/v", valueName, "/f"], { encoding: "utf8", windowsHide: true });
|
|
1799
1974
|
} catch {
|
|
1800
1975
|
}
|
|
1801
1976
|
rmSync(launcherDir, { recursive: true, force: true });
|
|
@@ -1804,9 +1979,9 @@ function runService(options) {
|
|
|
1804
1979
|
}
|
|
1805
1980
|
return { ok: true, messages };
|
|
1806
1981
|
}
|
|
1807
|
-
if (
|
|
1808
|
-
const unitDir =
|
|
1809
|
-
const unitFile =
|
|
1982
|
+
if (platform === "linux") {
|
|
1983
|
+
const unitDir = join3(homedir3(), ".config", "systemd", "user");
|
|
1984
|
+
const unitFile = join3(unitDir, "agent-comm-hub.service");
|
|
1810
1985
|
if (options.action === "install") {
|
|
1811
1986
|
const unit = `[Unit]
|
|
1812
1987
|
Description=agent-comm-hub (multi-peer MCP hub)
|
|
@@ -1841,7 +2016,72 @@ WantedBy=default.target
|
|
|
1841
2016
|
}
|
|
1842
2017
|
return { ok: true, messages };
|
|
1843
2018
|
}
|
|
1844
|
-
|
|
2019
|
+
if (platform === "darwin") {
|
|
2020
|
+
const launchAgentsDir = join3(homedir3(), "Library", "LaunchAgents");
|
|
2021
|
+
const label = "com.agent-comm-hub";
|
|
2022
|
+
const plist = join3(launchAgentsDir, `${label}.plist`);
|
|
2023
|
+
const logFile = join3(homedir3(), "Library", "Logs", "agent-comm-hub.log");
|
|
2024
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
2025
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2026
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2027
|
+
<plist version="1.0">
|
|
2028
|
+
<dict>
|
|
2029
|
+
<key>Label</key>
|
|
2030
|
+
<string>${label}</string>
|
|
2031
|
+
<key>ProgramArguments</key>
|
|
2032
|
+
<array>
|
|
2033
|
+
<string>${nodeExe()}</string>
|
|
2034
|
+
<string>${cliPath()}</string>
|
|
2035
|
+
<string>--host</string><string>${host}</string>
|
|
2036
|
+
<string>--port</string><string>${port}</string>
|
|
2037
|
+
<string>--path</string><string>${path2}</string>
|
|
2038
|
+
</array>
|
|
2039
|
+
<key>RunAtLoad</key>
|
|
2040
|
+
<true/>
|
|
2041
|
+
<key>KeepAlive</key>
|
|
2042
|
+
<true/>
|
|
2043
|
+
<key>StandardOutPath</key>
|
|
2044
|
+
<string>${logFile}</string>
|
|
2045
|
+
<key>StandardErrorPath</key>
|
|
2046
|
+
<string>${logFile}</string>
|
|
2047
|
+
</dict>
|
|
2048
|
+
</plist>
|
|
2049
|
+
`;
|
|
2050
|
+
if (options.action === "install") {
|
|
2051
|
+
if (dryRun) {
|
|
2052
|
+
messages.push(`[dry-run] write ${plist}`);
|
|
2053
|
+
messages.push(`[dry-run] launchctl bootstrap gui/${uid} ${plist}`);
|
|
2054
|
+
} else {
|
|
2055
|
+
mkdirSync(launchAgentsDir, { recursive: true });
|
|
2056
|
+
writeFileSync(plist, plistContent);
|
|
2057
|
+
try {
|
|
2058
|
+
execFileSync2("launchctl", ["bootstrap", `gui/${uid}`, plist], { encoding: "utf8", windowsHide: true });
|
|
2059
|
+
} catch {
|
|
2060
|
+
execFileSync2("launchctl", ["load", "-w", plist], { encoding: "utf8", windowsHide: true });
|
|
2061
|
+
}
|
|
2062
|
+
messages.push(`auto-start registered: launchd LaunchAgent ${plist}`);
|
|
2063
|
+
messages.push(`start it now with: launchctl bootstrap gui/${uid} ${plist}`);
|
|
2064
|
+
}
|
|
2065
|
+
} else {
|
|
2066
|
+
if (dryRun) {
|
|
2067
|
+
messages.push(`[dry-run] launchctl bootout gui/${uid}/${label}`);
|
|
2068
|
+
messages.push(`[dry-run] rm ${plist}`);
|
|
2069
|
+
} else {
|
|
2070
|
+
try {
|
|
2071
|
+
execFileSync2("launchctl", ["bootout", `gui/${uid}/${label}`], { encoding: "utf8", windowsHide: true });
|
|
2072
|
+
} catch {
|
|
2073
|
+
try {
|
|
2074
|
+
execFileSync2("launchctl", ["unload", "-w", plist], { encoding: "utf8", windowsHide: true });
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
rmSync(plist, { force: true });
|
|
2079
|
+
messages.push("auto-start removed (launchd LaunchAgent)");
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return { ok: true, messages };
|
|
2083
|
+
}
|
|
2084
|
+
return { ok: false, messages: [`auto-start is not implemented for ${platform} \u2014 use pm2 or your platform's supervisor`] };
|
|
1845
2085
|
} catch (error) {
|
|
1846
2086
|
return { ok: false, messages: [`${error.message}`] };
|
|
1847
2087
|
}
|
|
@@ -1851,7 +2091,7 @@ WantedBy=default.target
|
|
|
1851
2091
|
function parseArgs(argv) {
|
|
1852
2092
|
const args = {};
|
|
1853
2093
|
const numeric = /* @__PURE__ */ new Set(["--port", "--max-queue", "--history-limit", "--wait-timeout-ms", "--default-wait-ms", "--connected-window-ms", "--peer-idle-timeout-ms", "--herdr-timeout-ms"]);
|
|
1854
|
-
const string = /* @__PURE__ */ new Set(["--host", "--path", "--url", "--server-name", "--herdr-bin"]);
|
|
2094
|
+
const string = /* @__PURE__ */ new Set(["--host", "--path", "--url", "--server-name", "--agent", "--herdr-bin"]);
|
|
1855
2095
|
for (let i = 0; i < argv.length; i++) {
|
|
1856
2096
|
const flag = argv[i];
|
|
1857
2097
|
if (flag === "--help" || flag === "-h" || flag === "--version" || flag === "-V") {
|
|
@@ -1887,10 +2127,13 @@ Usage:
|
|
|
1887
2127
|
every installed agent (incremental,
|
|
1888
2128
|
idempotent; --remove undoes)
|
|
1889
2129
|
agent-comm-hub status [options] show hub health + online peers
|
|
2130
|
+
agent-comm-hub discover list installed agents (registry-
|
|
2131
|
+
driven; no config changes)
|
|
1890
2132
|
agent-comm-hub service install|uninstall [options]
|
|
1891
2133
|
one-shot auto-start (Windows Run
|
|
1892
2134
|
key + hidden VBS launcher, no admin;
|
|
1893
|
-
Linux systemd
|
|
2135
|
+
Linux systemd, macOS launchd;
|
|
2136
|
+
--dry-run prints)
|
|
1894
2137
|
agent-comm-hub update self-update from the npm registry
|
|
1895
2138
|
(files updated in place; restart
|
|
1896
2139
|
the hub afterwards)
|
|
@@ -1900,7 +2143,7 @@ Hub options:
|
|
|
1900
2143
|
--port <n> Listen port (default 18764)
|
|
1901
2144
|
--path <p> MCP endpoint path (default /mcp)
|
|
1902
2145
|
--max-queue <n> Queued messages per peer before dropping oldest (default 200)
|
|
1903
|
-
--history-limit <n> Retained history messages (default
|
|
2146
|
+
--history-limit <n> Retained history messages (default 1000)
|
|
1904
2147
|
--wait-timeout-ms <n> Long-poll ceiling for bridge_wait (default 60000)
|
|
1905
2148
|
--default-wait-ms <n> bridge_wait default budget (default 30000)
|
|
1906
2149
|
--connected-window-ms <n> Peer counts as active within this window (default 30000)
|
|
@@ -1912,6 +2155,7 @@ Hub options:
|
|
|
1912
2155
|
Setup options:
|
|
1913
2156
|
--url <url> Hub endpoint to register (default http://127.0.0.1:18764/mcp)
|
|
1914
2157
|
--server-name <name> Config key (default agent-hub)
|
|
2158
|
+
--agent <id> Only configure one registry agent (e.g. codex)
|
|
1915
2159
|
--remove Uninstall instead of install
|
|
1916
2160
|
|
|
1917
2161
|
-h, --help Show this help
|
|
@@ -1936,11 +2180,16 @@ try {
|
|
|
1936
2180
|
await runSetup({
|
|
1937
2181
|
url: args2["--url"],
|
|
1938
2182
|
serverName: args2["--server-name"],
|
|
2183
|
+
agent: args2["--agent"],
|
|
1939
2184
|
remove: args2["--remove"] === true,
|
|
1940
2185
|
log: (message) => log.info(message)
|
|
1941
2186
|
});
|
|
1942
2187
|
process.exit(0);
|
|
1943
2188
|
}
|
|
2189
|
+
if (command === "discover") {
|
|
2190
|
+
runDiscover({ log: (message) => log.info(message) });
|
|
2191
|
+
process.exit(0);
|
|
2192
|
+
}
|
|
1944
2193
|
if (command === "status") {
|
|
1945
2194
|
const args2 = parseArgs(rest);
|
|
1946
2195
|
const result = await runStatus({
|