agent-comm-hub 0.1.6

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/lib/setup.js ADDED
@@ -0,0 +1,168 @@
1
+ // src/setup.ts
2
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ var DEFAULT_URL = "http://127.0.0.1:18764/mcp";
8
+ var DEFAULT_SERVER = "agent-hub";
9
+ function defaultSkillSrc() {
10
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "agents", "SKILL.md");
11
+ }
12
+ function stamp() {
13
+ const now = /* @__PURE__ */ new Date();
14
+ const pad = (n) => String(n).padStart(2, "0");
15
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
16
+ }
17
+ async function readJson(file) {
18
+ if (!existsSync(file)) return null;
19
+ try {
20
+ return JSON.parse(await readFile(file, "utf8"));
21
+ } catch (error) {
22
+ throw new Error(`cannot parse JSON ${file}: ${error.message}`);
23
+ }
24
+ }
25
+ async function writeJsonNoBom(file, doc) {
26
+ await mkdir(dirname(file), { recursive: true });
27
+ await writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
28
+ }
29
+ async function backup(file) {
30
+ const bak = `${file}.bak-${stamp()}`;
31
+ await copyFile(file, bak);
32
+ return bak;
33
+ }
34
+ async function mergeJsonServer(file, section, entry, opts) {
35
+ if (!existsSync(file)) return "skipped";
36
+ const doc = await readJson(file);
37
+ if (doc === null) return "skipped";
38
+ let servers = doc[section];
39
+ if (servers === void 0 || typeof servers !== "object" || Array.isArray(servers)) {
40
+ servers = {};
41
+ doc[section] = servers;
42
+ }
43
+ const has = Object.prototype.hasOwnProperty.call(servers, opts.serverName);
44
+ if (opts.remove) {
45
+ if (!has) return "absent";
46
+ delete servers[opts.serverName];
47
+ await backup(file);
48
+ await writeJsonNoBom(file, doc);
49
+ return "removed";
50
+ }
51
+ if (has) {
52
+ const existing = servers[opts.serverName];
53
+ if (existing?.url === opts.url) return "unchanged";
54
+ }
55
+ servers[opts.serverName] = entry;
56
+ await backup(file);
57
+ await writeJsonNoBom(file, doc);
58
+ return "changed";
59
+ }
60
+ async function mergeTomlSection(file, opts) {
61
+ if (!existsSync(file)) return "skipped";
62
+ const text = await readFile(file, "utf8");
63
+ const marker = `[mcp_servers.${opts.serverName}]`;
64
+ const markerRe = new RegExp(`^\\[mcp_servers\\.${escapeRegExp(opts.serverName)}\\]`, "m");
65
+ if (opts.remove) {
66
+ if (!markerRe.test(text)) return "absent";
67
+ const cleaned = text.replace(new RegExp(`^\\[mcp_servers\\.${escapeRegExp(opts.serverName)}\\][^\\r\\n]*(\\r?\\n(?!\\[).*)*(\\r?\\n)?`, "m"), "");
68
+ await backup(file);
69
+ await writeFile(file, cleaned, "utf8");
70
+ return "removed";
71
+ }
72
+ if (markerRe.test(text)) return "unchanged";
73
+ const block = `
74
+ ${marker}
75
+ type = "streamable-http"
76
+ url = "${opts.url}"
77
+ `;
78
+ await backup(file);
79
+ await writeFile(file, text.trimEnd() + block, "utf8");
80
+ return "changed";
81
+ }
82
+ function escapeRegExp(value) {
83
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
84
+ }
85
+ async function syncSkill(skillDir, skillSrc, remove, log) {
86
+ if (remove) {
87
+ if (existsSync(skillDir)) {
88
+ await mkdir(dirname(skillDir), { recursive: true });
89
+ await rmRecursive(skillDir);
90
+ log(` skill removed: ${skillDir}`);
91
+ }
92
+ return;
93
+ }
94
+ if (!existsSync(skillSrc)) {
95
+ log(` SKILL.md source missing: ${skillSrc} (skipped)`);
96
+ return;
97
+ }
98
+ await mkdir(skillDir, { recursive: true });
99
+ await copyFile(skillSrc, join(skillDir, "SKILL.md"));
100
+ log(` skill -> ${join(skillDir, "SKILL.md")}`);
101
+ }
102
+ async function rmRecursive(dir) {
103
+ const { rm } = await import("node:fs/promises");
104
+ await rm(dir, { recursive: true, force: true });
105
+ }
106
+ async function runSetup(options = {}) {
107
+ const url = options.url ?? DEFAULT_URL;
108
+ const serverName = options.serverName ?? DEFAULT_SERVER;
109
+ const home = options.homeDir ?? homedir();
110
+ const skillSrc = options.skillSrc ?? defaultSkillSrc();
111
+ const remove = options.remove === true;
112
+ const log = options.log ?? ((message) => console.log(message));
113
+ const summary = { done: [], unchanged: [], skipped: [], errors: [] };
114
+ const record = (status, label, file) => {
115
+ if (status === "changed" || status === "removed") summary.done.push(`${label}: ${file}`);
116
+ else if (status === "unchanged" || status === "absent") summary.unchanged.push(`${label}: ${file}`);
117
+ else if (status === "skipped") summary.skipped.push(`${label}: ${file}`);
118
+ };
119
+ const jsonTargets = [
120
+ { label: "mcode", file: join(home, ".minimax", "mcp.json"), section: "mcpServers", entry: { url, type: "streamable-http", enabled: true, configured: true, timeout: 12e4, description: "agent-comm-hub: talk to every other agent connected to the hub." } },
121
+ { label: "mcode", file: join(home, ".minimax", "mcp", "mcp.json"), section: "mcpServers", entry: { url, type: "streamable-http", enabled: true, configured: true, timeout: 12e4, description: "agent-comm-hub: talk to every other agent connected to the hub." } },
122
+ { label: "opencode", file: join(home, ".config", "opencode", "opencode.json"), section: "mcp", entry: { type: "remote", url, enabled: true } },
123
+ { label: "kimi-code", file: join(home, ".kimi-code", "mcp.json"), section: "mcpServers", entry: { transport: "http", url, startupTimeoutMs: 3e4, toolTimeoutMs: 12e4 } },
124
+ { label: "gemini-cli", file: join(home, ".gemini", "settings.json"), section: "mcpServers", entry: { type: "http", url } }
125
+ ];
126
+ for (const target of jsonTargets) {
127
+ try {
128
+ const status = await mergeJsonServer(target.file, target.section, target.entry, { serverName, url, remove });
129
+ record(status, target.label, target.file);
130
+ } catch (error) {
131
+ summary.errors.push(`${target.label}: ${target.file} \u2014 ${error.message}`);
132
+ log(` ${target.label}: SKIPPED \u2014 ${error.message}`);
133
+ }
134
+ }
135
+ const codexFile = join(home, ".codex", "config.toml");
136
+ try {
137
+ const status = await mergeTomlSection(codexFile, { serverName, url, remove });
138
+ record(status, "codex", codexFile);
139
+ } catch (error) {
140
+ summary.errors.push(`codex: ${codexFile} \u2014 ${error.message}`);
141
+ log(` codex: SKIPPED \u2014 ${error.message}`);
142
+ }
143
+ const skillDirs = [
144
+ join(home, ".agents", "skills", serverName),
145
+ // cross-agent standard
146
+ join(home, ".minimax", "skills", serverName),
147
+ join(home, ".config", "opencode", "skills", serverName),
148
+ join(home, ".kimi-code", "skills", serverName),
149
+ join(home, ".gemini", "skills", serverName),
150
+ join(home, ".codex", "skills", serverName),
151
+ join(home, ".claude", "skills", serverName)
152
+ // config is manual; skill still useful
153
+ ];
154
+ for (const dir of skillDirs) {
155
+ try {
156
+ await syncSkill(dir, skillSrc, remove, log);
157
+ } catch (error) {
158
+ summary.errors.push(`skill ${dir} \u2014 ${error.message}`);
159
+ log(` skill ${dir}: SKIPPED \u2014 ${error.message}`);
160
+ }
161
+ }
162
+ if (remove) log("done. Manual targets (see agents/README.md): Claude Code (.mcp.json), DSH (cordis.patch.yml).");
163
+ else log("done. Manual targets (see agents/README.md): Claude Code (.mcp.json), DSH (cordis.patch.yml). Restart agent sessions to pick up the MCP server.");
164
+ return summary;
165
+ }
166
+ export {
167
+ runSetup
168
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "agent-comm-hub",
3
+ "version": "0.1.6",
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
+ "keywords": [
6
+ "mcp",
7
+ "agent",
8
+ "hub",
9
+ "multi-agent",
10
+ "communication",
11
+ "streamable-http",
12
+ "cli",
13
+ "ai"
14
+ ],
15
+ "author": "agent-comm-hub contributors",
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "bin": {
19
+ "agent-comm-hub": "lib/cli.js"
20
+ },
21
+ "files": [
22
+ "lib",
23
+ "agents",
24
+ "assets",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "license": "MIT",
31
+ "dependencies": {},
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "esbuild": "^0.25.0",
35
+ "typescript": "^5.8.0"
36
+ },
37
+ "scripts": {
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
+ "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",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "pnpm run build:test && node test/smoke.mjs && node test/setup.mjs && node test/ops.mjs",
42
+ "pack": "pnpm run build && pnpm pack",
43
+ "prepublishOnly": "pnpm run test && pnpm run build",
44
+ "prepare": "pnpm run build"
45
+ }
46
+ }