agent-comm-hub 0.3.0 → 0.4.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 +12 -5
- package/README.zh.md +11 -4
- package/agents/registry.json +201 -0
- package/lib/cli.js +329 -93
- package/lib/index.js +1 -1
- package/lib/setup.js +219 -69
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ Task Scheduler on Windows) — or the built-in one-shot auto-start:
|
|
|
84
84
|
```bash
|
|
85
85
|
agent-comm-hub service install # Windows: HKCU Run + hidden launcher (no admin)
|
|
86
86
|
# Linux: systemd --user unit, enabled
|
|
87
|
+
# macOS: launchd LaunchAgent
|
|
87
88
|
agent-comm-hub service uninstall # undo
|
|
88
89
|
agent-comm-hub status # is the hub up? who is online?
|
|
89
90
|
```
|
|
@@ -98,13 +99,19 @@ itself).
|
|
|
98
99
|
agent-comm-hub setup
|
|
99
100
|
# or: agents/install-all.ps1 (PowerShell equivalent)
|
|
100
101
|
# undo: agent-comm-hub setup --remove
|
|
102
|
+
# list what is installed (no changes): agent-comm-hub discover
|
|
103
|
+
# configure a single agent: agent-comm-hub setup --agent codex
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
`setup`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
`setup` **discovers** which agents are installed on this machine (PATH
|
|
107
|
+
commands, config paths, npm global packages — no shell, cross-platform) and
|
|
108
|
+
merges the `agent-hub` MCP entry into each one's config (mcode, opencode, Kimi
|
|
109
|
+
Code, Gemini CLI, Codex, zcode, DSH, claude-desktop on macOS), plus the English
|
|
110
|
+
skill into `~/.agents/skills/` (the cross-agent standard) and each agent's
|
|
111
|
+
private skills dir. Which agents are supported is declared in
|
|
112
|
+
[`agents/registry.json`](agents/registry.json) — adding a new agent is one
|
|
113
|
+
registry record, no code change. Only the `agent-hub` key is touched, every
|
|
114
|
+
file is backed up first, and re-running is a no-op. Claude Code's MCP config
|
|
108
115
|
stays manual (see below).
|
|
109
116
|
|
|
110
117
|
**Registration is automatic**: once an agent session starts, the MCP handshake
|
package/README.zh.md
CHANGED
|
@@ -82,6 +82,7 @@ agent-comm-hub
|
|
|
82
82
|
```bash
|
|
83
83
|
agent-comm-hub service install # Windows:HKCU Run + 隐藏启动器(无需管理员)
|
|
84
84
|
# Linux:systemd --user 单元并启用
|
|
85
|
+
# macOS:launchd LaunchAgent
|
|
85
86
|
agent-comm-hub service uninstall # 撤销
|
|
86
87
|
agent-comm-hub status # hub 是否在跑?谁在线?
|
|
87
88
|
```
|
|
@@ -94,12 +95,18 @@ agent-comm-hub status # hub 是否在跑?谁在线?
|
|
|
94
95
|
agent-comm-hub setup
|
|
95
96
|
# 或 PowerShell 版:agents/install-all.ps1
|
|
96
97
|
# 卸载:agent-comm-hub setup --remove
|
|
98
|
+
# 查看本机装了什么(只读):agent-comm-hub discover
|
|
99
|
+
# 只配置某一个:agent-comm-hub setup --agent codex
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
`setup`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
`setup` 会**自动发现**本机安装了哪些 agent(PATH 命令 / 配置文件路径 /
|
|
103
|
+
npm 全局包,纯 Node 无 shell、跨平台),并把 `agent-hub` 的 MCP 条目
|
|
104
|
+
**增量合并**进每个已安装 agent 的配置(mcode / opencode / kimi-code /
|
|
105
|
+
gemini / codex / zcode / DSH,macOS 上还有 claude-desktop),英文 SKILL 装到
|
|
106
|
+
`~/.agents/skills/`(跨 agent 标准位置)+ 各 agent 私有技能目录。支持哪些
|
|
107
|
+
agent 由 [`agents/registry.json`](agents/registry.json) 声明——新增 agent
|
|
108
|
+
只需加一条注册表记录,不改代码。只动 `agent-hub` 这一个键、每个文件先备份、
|
|
109
|
+
幂等可重跑。Claude Code 的 MCP 配置手动(见下)。
|
|
103
110
|
|
|
104
111
|
**注册全自动**:agent 会话一启动,MCP 握手即完成注册(客户端名 = peer id),无需任何手动操作。可选:`bridge_register("工具名:项目名")` 换可读 id。
|
|
105
112
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "docs/agent-registry.schema.json",
|
|
3
|
+
"agents": [
|
|
4
|
+
{
|
|
5
|
+
"id": "mcode",
|
|
6
|
+
"probe": [],
|
|
7
|
+
"configs": [
|
|
8
|
+
{
|
|
9
|
+
"file": "~/.minimax/mcp.json",
|
|
10
|
+
"section": "mcpServers",
|
|
11
|
+
"strategy": "json",
|
|
12
|
+
"entry": {
|
|
13
|
+
"url": "{url}",
|
|
14
|
+
"type": "streamable-http",
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"configured": true,
|
|
17
|
+
"timeout": 120000,
|
|
18
|
+
"description": "agent-comm-hub: talk to every other agent connected to the hub."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"file": "~/.minimax/mcp/mcp.json",
|
|
23
|
+
"section": "mcpServers",
|
|
24
|
+
"strategy": "json",
|
|
25
|
+
"entry": {
|
|
26
|
+
"url": "{url}",
|
|
27
|
+
"type": "streamable-http",
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"configured": true,
|
|
30
|
+
"timeout": 120000,
|
|
31
|
+
"description": "agent-comm-hub: talk to every other agent connected to the hub."
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"skill": "~/.minimax/skills",
|
|
36
|
+
"os": ["win32", "darwin", "linux"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "opencode",
|
|
40
|
+
"probe": ["opencode"],
|
|
41
|
+
"npm": ["cli"],
|
|
42
|
+
"configs": [
|
|
43
|
+
{
|
|
44
|
+
"file": "~/.config/opencode/opencode.json",
|
|
45
|
+
"section": "mcp",
|
|
46
|
+
"strategy": "json",
|
|
47
|
+
"entry": { "type": "remote", "url": "{url}", "enabled": true }
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"skill": "~/.config/opencode/skills",
|
|
51
|
+
"os": ["win32", "darwin", "linux"]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "kimi-code",
|
|
55
|
+
"probe": ["kimi"],
|
|
56
|
+
"configs": [
|
|
57
|
+
{
|
|
58
|
+
"file": "~/.kimi-code/mcp.json",
|
|
59
|
+
"section": "mcpServers",
|
|
60
|
+
"strategy": "json",
|
|
61
|
+
"entry": { "transport": "http", "url": "{url}", "startupTimeoutMs": 30000, "toolTimeoutMs": 120000 }
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"skill": "~/.kimi-code/skills",
|
|
65
|
+
"os": ["win32", "darwin", "linux"]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "gemini-cli",
|
|
69
|
+
"probe": ["gemini"],
|
|
70
|
+
"configs": [
|
|
71
|
+
{
|
|
72
|
+
"file": "~/.gemini/settings.json",
|
|
73
|
+
"section": "mcpServers",
|
|
74
|
+
"strategy": "json",
|
|
75
|
+
"entry": { "type": "http", "url": "{url}" }
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"skill": "~/.gemini/skills",
|
|
79
|
+
"os": ["win32", "darwin", "linux"]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "codex",
|
|
83
|
+
"probe": ["codex"],
|
|
84
|
+
"npm": ["codex"],
|
|
85
|
+
"configs": [
|
|
86
|
+
{
|
|
87
|
+
"file": "~/.codex/config.toml",
|
|
88
|
+
"section": null,
|
|
89
|
+
"strategy": "toml",
|
|
90
|
+
"entry": null
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"skill": "~/.codex/skills",
|
|
94
|
+
"os": ["win32", "darwin", "linux"]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "zcode",
|
|
98
|
+
"probe": [],
|
|
99
|
+
"configs": [
|
|
100
|
+
{
|
|
101
|
+
"file": "~/.zcode/cli/config.json",
|
|
102
|
+
"section": "mcp.servers",
|
|
103
|
+
"strategy": "json",
|
|
104
|
+
"entry": { "type": "remote", "url": "{url}", "enabled": true }
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"skill": "~/.zcode/skills",
|
|
108
|
+
"os": ["win32", "darwin", "linux"]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "dsh",
|
|
112
|
+
"probe": [],
|
|
113
|
+
"configs": [
|
|
114
|
+
{
|
|
115
|
+
"file": "~/.dsh/profiles/*/cordis.patch.yml",
|
|
116
|
+
"section": null,
|
|
117
|
+
"strategy": "dsh",
|
|
118
|
+
"entry": null
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"skill": "~/.dsh/skills",
|
|
122
|
+
"os": ["win32", "darwin", "linux"]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "claude",
|
|
126
|
+
"probe": ["claude"],
|
|
127
|
+
"npm": ["claude-code"],
|
|
128
|
+
"configs": [],
|
|
129
|
+
"skill": "~/.claude/skills",
|
|
130
|
+
"os": ["win32", "darwin", "linux"]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "qwen-code",
|
|
134
|
+
"probe": ["qwen"],
|
|
135
|
+
"npm": ["qwen-code"],
|
|
136
|
+
"configs": [
|
|
137
|
+
{
|
|
138
|
+
"file": "~/.qwen/settings.json",
|
|
139
|
+
"section": "mcpServers",
|
|
140
|
+
"strategy": "json",
|
|
141
|
+
"entry": { "httpUrl": "{url}" }
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"skill": null,
|
|
145
|
+
"os": ["win32", "darwin", "linux"]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "pi",
|
|
149
|
+
"probe": ["pi"],
|
|
150
|
+
"npm": ["pi-coding-agent"],
|
|
151
|
+
"configs": [],
|
|
152
|
+
"skill": null,
|
|
153
|
+
"os": ["win32", "darwin", "linux"]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "cursor",
|
|
157
|
+
"probe": ["cursor", "cursor-agent"],
|
|
158
|
+
"npm": ["cursor"],
|
|
159
|
+
"configs": [
|
|
160
|
+
{
|
|
161
|
+
"file": "~/.cursor/mcp.json",
|
|
162
|
+
"section": "mcpServers",
|
|
163
|
+
"strategy": "json",
|
|
164
|
+
"entry": { "url": "{url}" }
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"skill": null,
|
|
168
|
+
"os": ["win32", "darwin", "linux"]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"id": "qoder",
|
|
172
|
+
"probe": ["qoder"],
|
|
173
|
+
"npm": ["qoder"],
|
|
174
|
+
"configs": [],
|
|
175
|
+
"skill": null,
|
|
176
|
+
"os": ["win32", "darwin", "linux"]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"id": "codebuddy",
|
|
180
|
+
"probe": ["codebuddy"],
|
|
181
|
+
"npm": ["codebuddy"],
|
|
182
|
+
"configs": [],
|
|
183
|
+
"skill": null,
|
|
184
|
+
"os": ["win32", "darwin", "linux"]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"id": "claude-desktop",
|
|
188
|
+
"probe": [],
|
|
189
|
+
"configs": [
|
|
190
|
+
{
|
|
191
|
+
"file": "~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
192
|
+
"section": "mcpServers",
|
|
193
|
+
"strategy": "json",
|
|
194
|
+
"entry": { "url": "{url}", "type": "streamable-http" }
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"skill": null,
|
|
198
|
+
"os": ["darwin"]
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -1334,7 +1334,7 @@ function readBody(req) {
|
|
|
1334
1334
|
|
|
1335
1335
|
// src/index.ts
|
|
1336
1336
|
var SERVER_NAME = "agent-comm-hub";
|
|
1337
|
-
var SERVER_VERSION = "0.
|
|
1337
|
+
var SERVER_VERSION = "0.4.0";
|
|
1338
1338
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1339
1339
|
var DEFAULT_PORT = 18764;
|
|
1340
1340
|
var DEFAULT_PATH = "/mcp";
|
|
@@ -1410,15 +1410,190 @@ function startHub(config = {}, log2 = console) {
|
|
|
1410
1410
|
}
|
|
1411
1411
|
|
|
1412
1412
|
// src/setup.ts
|
|
1413
|
-
import { copyFile, mkdir, readFile,
|
|
1414
|
-
import { existsSync } from "node:fs";
|
|
1413
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1414
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1415
|
+
import { homedir as homedir2 } from "node:os";
|
|
1416
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1417
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1418
|
+
|
|
1419
|
+
// src/discover.ts
|
|
1420
|
+
import { execFileSync } from "node:child_process";
|
|
1421
|
+
import { existsSync, readdirSync, accessSync, readFileSync, constants as fsConstants } from "node:fs";
|
|
1415
1422
|
import { homedir } from "node:os";
|
|
1416
|
-
import { dirname, join } from "node:path";
|
|
1423
|
+
import { dirname, join, sep } from "node:path";
|
|
1417
1424
|
import { fileURLToPath } from "node:url";
|
|
1425
|
+
function registryFile() {
|
|
1426
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "agents", "registry.json");
|
|
1427
|
+
}
|
|
1428
|
+
function expandHome(file, home) {
|
|
1429
|
+
return file.startsWith("~/") ? join(home, file.slice(2)) : file;
|
|
1430
|
+
}
|
|
1431
|
+
function expandConfigFile(file, home) {
|
|
1432
|
+
const expanded = expandHome(file, home);
|
|
1433
|
+
const star = expanded.indexOf("*");
|
|
1434
|
+
if (star < 0) return [expanded];
|
|
1435
|
+
const prefix = expanded.slice(0, star);
|
|
1436
|
+
const suffix = expanded.slice(star + 1);
|
|
1437
|
+
const base = prefix.slice(0, prefix.lastIndexOf(sep));
|
|
1438
|
+
if (!existsSync(base)) return [];
|
|
1439
|
+
return readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(base, entry.name, suffix));
|
|
1440
|
+
}
|
|
1441
|
+
function validateRegistry(registry) {
|
|
1442
|
+
if (!Array.isArray(registry.agents)) throw new Error('registry: missing "agents" array');
|
|
1443
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1444
|
+
for (const agent of registry.agents) {
|
|
1445
|
+
if (typeof agent.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(agent.id)) {
|
|
1446
|
+
throw new Error(`registry: bad agent id ${JSON.stringify(agent.id)}`);
|
|
1447
|
+
}
|
|
1448
|
+
if (ids.has(agent.id)) throw new Error(`registry: duplicate agent id '${agent.id}'`);
|
|
1449
|
+
ids.add(agent.id);
|
|
1450
|
+
if (!Array.isArray(agent.probe)) throw new Error(`registry: ${agent.id}: probe must be an array`);
|
|
1451
|
+
if (agent.npm !== void 0 && !Array.isArray(agent.npm)) throw new Error(`registry: ${agent.id}: npm must be an array`);
|
|
1452
|
+
if (!Array.isArray(agent.configs)) throw new Error(`registry: ${agent.id}: configs must be an array`);
|
|
1453
|
+
for (const config of agent.configs) {
|
|
1454
|
+
if (typeof config.file !== "string" || !config.file.startsWith("~/")) {
|
|
1455
|
+
throw new Error(`registry: ${agent.id}: config file must be '~'-relative`);
|
|
1456
|
+
}
|
|
1457
|
+
if (config.file.split("/").includes("..")) {
|
|
1458
|
+
throw new Error(`registry: ${agent.id}: config file must not contain '..'`);
|
|
1459
|
+
}
|
|
1460
|
+
if (config.file.split("*").length > 2) {
|
|
1461
|
+
throw new Error(`registry: ${agent.id}: at most one '*' segment allowed`);
|
|
1462
|
+
}
|
|
1463
|
+
if (!["json", "toml", "dsh"].includes(config.strategy)) {
|
|
1464
|
+
throw new Error(`registry: ${agent.id}: unknown strategy '${config.strategy}'`);
|
|
1465
|
+
}
|
|
1466
|
+
if (config.strategy === "json" && (typeof config.section !== "string" || config.entry === null)) {
|
|
1467
|
+
throw new Error(`registry: ${agent.id}: json strategy needs a section and an entry`);
|
|
1468
|
+
}
|
|
1469
|
+
if (config.strategy !== "json" && config.entry !== null) {
|
|
1470
|
+
throw new Error(`registry: ${agent.id}: only json strategy may carry an entry`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (agent.skill !== null && (typeof agent.skill !== "string" || !agent.skill.startsWith("~/"))) {
|
|
1474
|
+
throw new Error(`registry: ${agent.id}: skill must be '~'-relative or null`);
|
|
1475
|
+
}
|
|
1476
|
+
if (agent.os !== void 0 && (!Array.isArray(agent.os) || agent.os.some((os2) => !["win32", "darwin", "linux"].includes(os2)))) {
|
|
1477
|
+
throw new Error(`registry: ${agent.id}: invalid os list`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function loadRegistry(file = registryFile()) {
|
|
1482
|
+
const registry = JSON.parse(readFileSync(file, "utf8"));
|
|
1483
|
+
validateRegistry(registry);
|
|
1484
|
+
return registry;
|
|
1485
|
+
}
|
|
1486
|
+
function commandOnPath(command, pathEnv, pathext, platform) {
|
|
1487
|
+
const dirs = pathEnv.split(platform === "win32" ? ";" : ":");
|
|
1488
|
+
const extensions = platform === "win32" ? ["", ...(pathext || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
1489
|
+
for (const dir of dirs) {
|
|
1490
|
+
const base = dir === "" ? "." : dir;
|
|
1491
|
+
for (const ext of extensions) {
|
|
1492
|
+
const candidate = join(base, command + ext);
|
|
1493
|
+
try {
|
|
1494
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
1495
|
+
return true;
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
function readNpmNames(root) {
|
|
1503
|
+
const names = [];
|
|
1504
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
1505
|
+
if (!entry.isDirectory()) continue;
|
|
1506
|
+
if (entry.name.startsWith("@")) {
|
|
1507
|
+
const scopeDir = join(root, entry.name);
|
|
1508
|
+
for (const sub of readdirSync(scopeDir, { withFileTypes: true })) {
|
|
1509
|
+
if (sub.isDirectory()) names.push(sub.name);
|
|
1510
|
+
}
|
|
1511
|
+
} else {
|
|
1512
|
+
names.push(entry.name);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return names;
|
|
1516
|
+
}
|
|
1517
|
+
function npmGlobalRoot(platform) {
|
|
1518
|
+
try {
|
|
1519
|
+
const out = execFileSync(platform === "win32" ? "npm.cmd" : "npm", ["root", "-g"], { encoding: "utf8", windowsHide: true }).trim();
|
|
1520
|
+
return out || null;
|
|
1521
|
+
} catch {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function npmFallbackRoots(home, platform) {
|
|
1526
|
+
if (platform === "win32") {
|
|
1527
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
1528
|
+
return [join(appData, "npm", "node_modules")];
|
|
1529
|
+
}
|
|
1530
|
+
return [
|
|
1531
|
+
"/usr/local/lib/node_modules",
|
|
1532
|
+
"/usr/lib/node_modules",
|
|
1533
|
+
...existsSync(join(home, ".nvm", "versions", "node")) ? readdirSync(join(home, ".nvm", "versions", "node")).map((dir) => join(home, ".nvm", "versions", "node", dir, "lib", "node_modules")) : []
|
|
1534
|
+
];
|
|
1535
|
+
}
|
|
1536
|
+
function discover(registry, options = {}) {
|
|
1537
|
+
const home = options.homeDir ?? homedir();
|
|
1538
|
+
const platform = options.platform ?? process.platform;
|
|
1539
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
1540
|
+
const pathext = options.pathext ?? process.env.PATHEXT ?? "";
|
|
1541
|
+
let npmNames = null;
|
|
1542
|
+
if (!options.noNpm) {
|
|
1543
|
+
if (options.npmRoot !== void 0 && options.npmRoot !== null) {
|
|
1544
|
+
npmNames = existsSync(options.npmRoot) ? readNpmNames(options.npmRoot) : [];
|
|
1545
|
+
} else if (options.npmRoot !== null) {
|
|
1546
|
+
const root = npmGlobalRoot(platform);
|
|
1547
|
+
if (root && existsSync(root)) {
|
|
1548
|
+
npmNames = readNpmNames(root);
|
|
1549
|
+
} else {
|
|
1550
|
+
npmNames = [];
|
|
1551
|
+
for (const fallback of npmFallbackRoots(home, platform)) {
|
|
1552
|
+
if (existsSync(fallback)) {
|
|
1553
|
+
npmNames = readNpmNames(fallback);
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return registry.agents.filter((agent) => agent.os === void 0 || agent.os.includes(platform)).map((agent) => {
|
|
1561
|
+
let source = "none";
|
|
1562
|
+
const configFiles = [];
|
|
1563
|
+
for (const config of agent.configs) {
|
|
1564
|
+
for (const file of expandConfigFile(config.file, home)) {
|
|
1565
|
+
if (existsSync(file)) {
|
|
1566
|
+
configFiles.push(file);
|
|
1567
|
+
if (source === "none") source = "config";
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (source === "none" && agent.probe.some((command) => commandOnPath(command, pathEnv, pathext, platform))) {
|
|
1572
|
+
source = "path";
|
|
1573
|
+
}
|
|
1574
|
+
if (source === "none" && npmNames !== null && (agent.probe.some((command) => npmNames.includes(command)) || (agent.npm ?? []).some((name) => npmNames.includes(name)))) {
|
|
1575
|
+
source = "npm";
|
|
1576
|
+
}
|
|
1577
|
+
return { id: agent.id, source, configFiles, present: source !== "none" };
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
function runDiscover(options = {}) {
|
|
1581
|
+
const log2 = options.log ?? ((message) => console.log(message));
|
|
1582
|
+
const found = discover(loadRegistry(), options);
|
|
1583
|
+
log2("discovered agents:");
|
|
1584
|
+
for (const agent of found) {
|
|
1585
|
+
const status = agent.present ? agent.source : "not installed";
|
|
1586
|
+
const files = agent.configFiles.length > 0 ? ` \u2014 ${agent.configFiles.join(", ")}` : "";
|
|
1587
|
+
log2(` ${agent.id.padEnd(16)} ${status}${files}`);
|
|
1588
|
+
}
|
|
1589
|
+
return found;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/setup.ts
|
|
1418
1593
|
var DEFAULT_URL = "http://127.0.0.1:18764/mcp";
|
|
1419
1594
|
var DEFAULT_SERVER = "agent-hub";
|
|
1420
1595
|
function defaultSkillSrc() {
|
|
1421
|
-
return
|
|
1596
|
+
return join2(dirname2(fileURLToPath2(import.meta.url)), "..", "agents", "SKILL.md");
|
|
1422
1597
|
}
|
|
1423
1598
|
function stamp() {
|
|
1424
1599
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1426,7 +1601,7 @@ function stamp() {
|
|
|
1426
1601
|
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1427
1602
|
}
|
|
1428
1603
|
async function readJson(file) {
|
|
1429
|
-
if (!
|
|
1604
|
+
if (!existsSync2(file)) return null;
|
|
1430
1605
|
try {
|
|
1431
1606
|
return JSON.parse(await readFile(file, "utf8"));
|
|
1432
1607
|
} catch (error) {
|
|
@@ -1434,7 +1609,7 @@ async function readJson(file) {
|
|
|
1434
1609
|
}
|
|
1435
1610
|
}
|
|
1436
1611
|
async function writeJsonNoBom(file, doc) {
|
|
1437
|
-
await mkdir(
|
|
1612
|
+
await mkdir(dirname2(file), { recursive: true });
|
|
1438
1613
|
await writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
1439
1614
|
}
|
|
1440
1615
|
async function backup(file) {
|
|
@@ -1455,7 +1630,7 @@ function resolveSection(doc, section) {
|
|
|
1455
1630
|
return node;
|
|
1456
1631
|
}
|
|
1457
1632
|
async function mergeJsonServer(file, section, entry, opts) {
|
|
1458
|
-
if (!
|
|
1633
|
+
if (!existsSync2(file)) return "skipped";
|
|
1459
1634
|
const doc = await readJson(file);
|
|
1460
1635
|
if (doc === null) return "skipped";
|
|
1461
1636
|
const servers = resolveSection(doc, section);
|
|
@@ -1477,7 +1652,7 @@ async function mergeJsonServer(file, section, entry, opts) {
|
|
|
1477
1652
|
return "changed";
|
|
1478
1653
|
}
|
|
1479
1654
|
async function mergeTomlSection(file, opts) {
|
|
1480
|
-
if (!
|
|
1655
|
+
if (!existsSync2(file)) return "skipped";
|
|
1481
1656
|
const text = await readFile(file, "utf8");
|
|
1482
1657
|
const marker = `[mcp_servers.${opts.serverName}]`;
|
|
1483
1658
|
const markerRe = new RegExp(`^\\[mcp_servers\\.${escapeRegExp(opts.serverName)}\\]`, "m");
|
|
@@ -1515,7 +1690,7 @@ ${DSH_PATCH_MARKER} (installed by \`agent-comm-hub setup\`; undo with \`setup --
|
|
|
1515
1690
|
`;
|
|
1516
1691
|
}
|
|
1517
1692
|
async function mergeDshPatch(file, opts) {
|
|
1518
|
-
if (!
|
|
1693
|
+
if (!existsSync2(file)) return "skipped";
|
|
1519
1694
|
const text = await readFile(file, "utf8");
|
|
1520
1695
|
const lines = text.split("\n");
|
|
1521
1696
|
const markerLine = lines.findIndex((line) => line.includes(DSH_PATCH_MARKER));
|
|
@@ -1565,29 +1740,40 @@ async function mergeDshPatch(file, opts) {
|
|
|
1565
1740
|
}
|
|
1566
1741
|
async function syncSkill(skillDir, skillSrc, remove, log2) {
|
|
1567
1742
|
if (remove) {
|
|
1568
|
-
if (
|
|
1569
|
-
await mkdir(
|
|
1743
|
+
if (existsSync2(skillDir)) {
|
|
1744
|
+
await mkdir(dirname2(skillDir), { recursive: true });
|
|
1570
1745
|
await rmRecursive(skillDir);
|
|
1571
1746
|
log2(` skill removed: ${skillDir}`);
|
|
1572
1747
|
}
|
|
1573
1748
|
return;
|
|
1574
1749
|
}
|
|
1575
|
-
if (!
|
|
1750
|
+
if (!existsSync2(skillSrc)) {
|
|
1576
1751
|
log2(` SKILL.md source missing: ${skillSrc} (skipped)`);
|
|
1577
1752
|
return;
|
|
1578
1753
|
}
|
|
1579
1754
|
await mkdir(skillDir, { recursive: true });
|
|
1580
|
-
await copyFile(skillSrc,
|
|
1581
|
-
log2(` skill -> ${
|
|
1755
|
+
await copyFile(skillSrc, join2(skillDir, "SKILL.md"));
|
|
1756
|
+
log2(` skill -> ${join2(skillDir, "SKILL.md")}`);
|
|
1582
1757
|
}
|
|
1583
1758
|
async function rmRecursive(dir) {
|
|
1584
1759
|
const { rm } = await import("node:fs/promises");
|
|
1585
1760
|
await rm(dir, { recursive: true, force: true });
|
|
1586
1761
|
}
|
|
1762
|
+
function substitute(entry, values) {
|
|
1763
|
+
const out = {};
|
|
1764
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
1765
|
+
if (typeof value === "string") {
|
|
1766
|
+
out[key] = value.replaceAll("{url}", values.url).replaceAll("{serverName}", values.serverName);
|
|
1767
|
+
} else {
|
|
1768
|
+
out[key] = value;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return out;
|
|
1772
|
+
}
|
|
1587
1773
|
async function runSetup(options = {}) {
|
|
1588
1774
|
const url = options.url ?? DEFAULT_URL;
|
|
1589
1775
|
const serverName = options.serverName ?? DEFAULT_SERVER;
|
|
1590
|
-
const home = options.homeDir ??
|
|
1776
|
+
const home = options.homeDir ?? homedir2();
|
|
1591
1777
|
const skillSrc = options.skillSrc ?? defaultSkillSrc();
|
|
1592
1778
|
const remove = options.remove === true;
|
|
1593
1779
|
const log2 = options.log ?? ((message) => console.log(message));
|
|
@@ -1597,65 +1783,40 @@ async function runSetup(options = {}) {
|
|
|
1597
1783
|
else if (status === "unchanged" || status === "absent") summary.unchanged.push(`${label}: ${file}`);
|
|
1598
1784
|
else if (status === "skipped") summary.skipped.push(`${label}: ${file}`);
|
|
1599
1785
|
};
|
|
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}`);
|
|
1786
|
+
const registry = loadRegistry();
|
|
1787
|
+
const found = discover(registry, { homeDir: home, pathEnv: options.pathEnv, noNpm: options.noNpm === true });
|
|
1788
|
+
const only = options.agent;
|
|
1789
|
+
let targetAgents = [];
|
|
1790
|
+
if (only !== void 0) {
|
|
1791
|
+
const match = registry.agents.find((agent) => agent.id === only);
|
|
1792
|
+
if (match === void 0) {
|
|
1793
|
+
log2(`agent '${only}' is not in the registry (see agents/registry.json)`);
|
|
1794
|
+
} else {
|
|
1795
|
+
targetAgents = [match];
|
|
1796
|
+
log2(`configure only: ${only}`);
|
|
1632
1797
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1798
|
+
} else {
|
|
1799
|
+
targetAgents = found.filter((agent) => agent.present).map((agent) => registry.agents.find((entry) => entry.id === agent.id));
|
|
1800
|
+
const present2 = targetAgents.map((agent) => agent.id);
|
|
1801
|
+
log2(`discovered: ${present2.length > 0 ? present2.join(", ") : "none"}`);
|
|
1802
|
+
}
|
|
1803
|
+
for (const agent of targetAgents) {
|
|
1804
|
+
for (const config of agent.configs) {
|
|
1805
|
+
for (const file of expandConfigFile(config.file, home)) {
|
|
1806
|
+
try {
|
|
1807
|
+
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 });
|
|
1808
|
+
record(status, agent.id, file);
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
summary.errors.push(`${agent.id}: ${file} \u2014 ${error.message}`);
|
|
1811
|
+
log2(` ${agent.id}: SKIPPED \u2014 ${error.message}`);
|
|
1812
|
+
}
|
|
1642
1813
|
}
|
|
1643
1814
|
}
|
|
1644
1815
|
}
|
|
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
|
-
];
|
|
1816
|
+
const skillDirs = [join2(home, ".agents", "skills", serverName)];
|
|
1817
|
+
for (const agent of targetAgents) {
|
|
1818
|
+
if (agent.skill !== null) skillDirs.push(join2(expandHome(agent.skill, home), serverName));
|
|
1819
|
+
}
|
|
1659
1820
|
for (const dir of skillDirs) {
|
|
1660
1821
|
try {
|
|
1661
1822
|
await syncSkill(dir, skillSrc, remove, log2);
|
|
@@ -1670,12 +1831,12 @@ async function runSetup(options = {}) {
|
|
|
1670
1831
|
}
|
|
1671
1832
|
|
|
1672
1833
|
// src/ops.ts
|
|
1673
|
-
import { execFileSync } from "node:child_process";
|
|
1834
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1674
1835
|
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
|
|
1836
|
+
import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
1837
|
+
import { homedir as homedir3 } from "node:os";
|
|
1838
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
1839
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1679
1840
|
async function runStatus(options = {}) {
|
|
1680
1841
|
const host = options.host ?? "127.0.0.1";
|
|
1681
1842
|
const port = options.port ?? 18764;
|
|
@@ -1727,8 +1888,8 @@ async function runStatus(options = {}) {
|
|
|
1727
1888
|
function runUpdate() {
|
|
1728
1889
|
const messages = [];
|
|
1729
1890
|
try {
|
|
1730
|
-
const pkgFile =
|
|
1731
|
-
const before = JSON.parse(
|
|
1891
|
+
const pkgFile = join3(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
1892
|
+
const before = JSON.parse(readFileSync2(pkgFile, "utf8")).version ?? "?";
|
|
1732
1893
|
messages.push(`current version: ${before}`);
|
|
1733
1894
|
const script = [
|
|
1734
1895
|
"import { execFileSync } from 'node:child_process'",
|
|
@@ -1744,7 +1905,7 @@ function runUpdate() {
|
|
|
1744
1905
|
"if (after === before) console.log('already up to date (v' + after + ')')",
|
|
1745
1906
|
"else console.log('updated: v' + before + ' -> v' + after)"
|
|
1746
1907
|
].join("\n");
|
|
1747
|
-
const out =
|
|
1908
|
+
const out = execFileSync2(process.execPath, ["--input-type=module", "-e", script], { encoding: "utf8", windowsHide: true });
|
|
1748
1909
|
messages.push(out.trim());
|
|
1749
1910
|
messages.push("restart the hub (agent-comm-hub) to pick up the new version");
|
|
1750
1911
|
return { ok: true, messages };
|
|
@@ -1753,14 +1914,14 @@ function runUpdate() {
|
|
|
1753
1914
|
}
|
|
1754
1915
|
}
|
|
1755
1916
|
function cliPath() {
|
|
1756
|
-
return
|
|
1917
|
+
return fileURLToPath3(import.meta.url);
|
|
1757
1918
|
}
|
|
1758
1919
|
function nodeExe() {
|
|
1759
1920
|
return process.execPath;
|
|
1760
1921
|
}
|
|
1761
1922
|
function run(command, args, dryRun) {
|
|
1762
1923
|
if (dryRun) return `[dry-run] ${command} ${args.join(" ")}`;
|
|
1763
|
-
return
|
|
1924
|
+
return execFileSync2(command, args, { encoding: "utf8", windowsHide: true }).trim();
|
|
1764
1925
|
}
|
|
1765
1926
|
function runService(options) {
|
|
1766
1927
|
const messages = [];
|
|
@@ -1768,11 +1929,12 @@ function runService(options) {
|
|
|
1768
1929
|
const host = options.host ?? "127.0.0.1";
|
|
1769
1930
|
const path2 = options.path ?? "/mcp";
|
|
1770
1931
|
const dryRun = options.dryRun === true;
|
|
1932
|
+
const platform = options.platform ?? process.platform;
|
|
1771
1933
|
try {
|
|
1772
|
-
if (
|
|
1773
|
-
const appData = process.env.APPDATA ??
|
|
1774
|
-
const launcherDir =
|
|
1775
|
-
const vbs =
|
|
1934
|
+
if (platform === "win32") {
|
|
1935
|
+
const appData = process.env.APPDATA ?? join3(homedir3(), "AppData", "Roaming");
|
|
1936
|
+
const launcherDir = join3(appData, "agent-comm-hub");
|
|
1937
|
+
const vbs = join3(launcherDir, "agent-comm-hub.vbs");
|
|
1776
1938
|
const runKey = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
1777
1939
|
const valueName = "agent-comm-hub";
|
|
1778
1940
|
if (options.action === "install") {
|
|
@@ -1785,7 +1947,7 @@ function runService(options) {
|
|
|
1785
1947
|
} else {
|
|
1786
1948
|
mkdirSync(launcherDir, { recursive: true });
|
|
1787
1949
|
writeFileSync(vbs, vbsContent);
|
|
1788
|
-
|
|
1950
|
+
execFileSync2("reg", ["add", runKey, "/v", valueName, "/t", "REG_SZ", "/d", `wscript.exe "${vbs}"`, "/f"], { encoding: "utf8", windowsHide: true });
|
|
1789
1951
|
messages.push(`auto-start registered: HKCU Run '${valueName}' -> hidden wscript launcher "${vbs}"`);
|
|
1790
1952
|
messages.push(`start it now with: wscript.exe "${vbs}"`);
|
|
1791
1953
|
}
|
|
@@ -1795,7 +1957,7 @@ function runService(options) {
|
|
|
1795
1957
|
messages.push(`[dry-run] del ${vbs}`);
|
|
1796
1958
|
} else {
|
|
1797
1959
|
try {
|
|
1798
|
-
|
|
1960
|
+
execFileSync2("reg", ["delete", runKey, "/v", valueName, "/f"], { encoding: "utf8", windowsHide: true });
|
|
1799
1961
|
} catch {
|
|
1800
1962
|
}
|
|
1801
1963
|
rmSync(launcherDir, { recursive: true, force: true });
|
|
@@ -1804,9 +1966,9 @@ function runService(options) {
|
|
|
1804
1966
|
}
|
|
1805
1967
|
return { ok: true, messages };
|
|
1806
1968
|
}
|
|
1807
|
-
if (
|
|
1808
|
-
const unitDir =
|
|
1809
|
-
const unitFile =
|
|
1969
|
+
if (platform === "linux") {
|
|
1970
|
+
const unitDir = join3(homedir3(), ".config", "systemd", "user");
|
|
1971
|
+
const unitFile = join3(unitDir, "agent-comm-hub.service");
|
|
1810
1972
|
if (options.action === "install") {
|
|
1811
1973
|
const unit = `[Unit]
|
|
1812
1974
|
Description=agent-comm-hub (multi-peer MCP hub)
|
|
@@ -1841,7 +2003,72 @@ WantedBy=default.target
|
|
|
1841
2003
|
}
|
|
1842
2004
|
return { ok: true, messages };
|
|
1843
2005
|
}
|
|
1844
|
-
|
|
2006
|
+
if (platform === "darwin") {
|
|
2007
|
+
const launchAgentsDir = join3(homedir3(), "Library", "LaunchAgents");
|
|
2008
|
+
const label = "com.agent-comm-hub";
|
|
2009
|
+
const plist = join3(launchAgentsDir, `${label}.plist`);
|
|
2010
|
+
const logFile = join3(homedir3(), "Library", "Logs", "agent-comm-hub.log");
|
|
2011
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
2012
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2013
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2014
|
+
<plist version="1.0">
|
|
2015
|
+
<dict>
|
|
2016
|
+
<key>Label</key>
|
|
2017
|
+
<string>${label}</string>
|
|
2018
|
+
<key>ProgramArguments</key>
|
|
2019
|
+
<array>
|
|
2020
|
+
<string>${nodeExe()}</string>
|
|
2021
|
+
<string>${cliPath()}</string>
|
|
2022
|
+
<string>--host</string><string>${host}</string>
|
|
2023
|
+
<string>--port</string><string>${port}</string>
|
|
2024
|
+
<string>--path</string><string>${path2}</string>
|
|
2025
|
+
</array>
|
|
2026
|
+
<key>RunAtLoad</key>
|
|
2027
|
+
<true/>
|
|
2028
|
+
<key>KeepAlive</key>
|
|
2029
|
+
<true/>
|
|
2030
|
+
<key>StandardOutPath</key>
|
|
2031
|
+
<string>${logFile}</string>
|
|
2032
|
+
<key>StandardErrorPath</key>
|
|
2033
|
+
<string>${logFile}</string>
|
|
2034
|
+
</dict>
|
|
2035
|
+
</plist>
|
|
2036
|
+
`;
|
|
2037
|
+
if (options.action === "install") {
|
|
2038
|
+
if (dryRun) {
|
|
2039
|
+
messages.push(`[dry-run] write ${plist}`);
|
|
2040
|
+
messages.push(`[dry-run] launchctl bootstrap gui/${uid} ${plist}`);
|
|
2041
|
+
} else {
|
|
2042
|
+
mkdirSync(launchAgentsDir, { recursive: true });
|
|
2043
|
+
writeFileSync(plist, plistContent);
|
|
2044
|
+
try {
|
|
2045
|
+
execFileSync2("launchctl", ["bootstrap", `gui/${uid}`, plist], { encoding: "utf8", windowsHide: true });
|
|
2046
|
+
} catch {
|
|
2047
|
+
execFileSync2("launchctl", ["load", "-w", plist], { encoding: "utf8", windowsHide: true });
|
|
2048
|
+
}
|
|
2049
|
+
messages.push(`auto-start registered: launchd LaunchAgent ${plist}`);
|
|
2050
|
+
messages.push(`start it now with: launchctl bootstrap gui/${uid} ${plist}`);
|
|
2051
|
+
}
|
|
2052
|
+
} else {
|
|
2053
|
+
if (dryRun) {
|
|
2054
|
+
messages.push(`[dry-run] launchctl bootout gui/${uid}/${label}`);
|
|
2055
|
+
messages.push(`[dry-run] rm ${plist}`);
|
|
2056
|
+
} else {
|
|
2057
|
+
try {
|
|
2058
|
+
execFileSync2("launchctl", ["bootout", `gui/${uid}/${label}`], { encoding: "utf8", windowsHide: true });
|
|
2059
|
+
} catch {
|
|
2060
|
+
try {
|
|
2061
|
+
execFileSync2("launchctl", ["unload", "-w", plist], { encoding: "utf8", windowsHide: true });
|
|
2062
|
+
} catch {
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
rmSync(plist, { force: true });
|
|
2066
|
+
messages.push("auto-start removed (launchd LaunchAgent)");
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
return { ok: true, messages };
|
|
2070
|
+
}
|
|
2071
|
+
return { ok: false, messages: [`auto-start is not implemented for ${platform} \u2014 use pm2 or your platform's supervisor`] };
|
|
1845
2072
|
} catch (error) {
|
|
1846
2073
|
return { ok: false, messages: [`${error.message}`] };
|
|
1847
2074
|
}
|
|
@@ -1851,7 +2078,7 @@ WantedBy=default.target
|
|
|
1851
2078
|
function parseArgs(argv) {
|
|
1852
2079
|
const args = {};
|
|
1853
2080
|
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"]);
|
|
2081
|
+
const string = /* @__PURE__ */ new Set(["--host", "--path", "--url", "--server-name", "--agent", "--herdr-bin"]);
|
|
1855
2082
|
for (let i = 0; i < argv.length; i++) {
|
|
1856
2083
|
const flag = argv[i];
|
|
1857
2084
|
if (flag === "--help" || flag === "-h" || flag === "--version" || flag === "-V") {
|
|
@@ -1887,10 +2114,13 @@ Usage:
|
|
|
1887
2114
|
every installed agent (incremental,
|
|
1888
2115
|
idempotent; --remove undoes)
|
|
1889
2116
|
agent-comm-hub status [options] show hub health + online peers
|
|
2117
|
+
agent-comm-hub discover list installed agents (registry-
|
|
2118
|
+
driven; no config changes)
|
|
1890
2119
|
agent-comm-hub service install|uninstall [options]
|
|
1891
2120
|
one-shot auto-start (Windows Run
|
|
1892
2121
|
key + hidden VBS launcher, no admin;
|
|
1893
|
-
Linux systemd
|
|
2122
|
+
Linux systemd, macOS launchd;
|
|
2123
|
+
--dry-run prints)
|
|
1894
2124
|
agent-comm-hub update self-update from the npm registry
|
|
1895
2125
|
(files updated in place; restart
|
|
1896
2126
|
the hub afterwards)
|
|
@@ -1912,6 +2142,7 @@ Hub options:
|
|
|
1912
2142
|
Setup options:
|
|
1913
2143
|
--url <url> Hub endpoint to register (default http://127.0.0.1:18764/mcp)
|
|
1914
2144
|
--server-name <name> Config key (default agent-hub)
|
|
2145
|
+
--agent <id> Only configure one registry agent (e.g. codex)
|
|
1915
2146
|
--remove Uninstall instead of install
|
|
1916
2147
|
|
|
1917
2148
|
-h, --help Show this help
|
|
@@ -1936,11 +2167,16 @@ try {
|
|
|
1936
2167
|
await runSetup({
|
|
1937
2168
|
url: args2["--url"],
|
|
1938
2169
|
serverName: args2["--server-name"],
|
|
2170
|
+
agent: args2["--agent"],
|
|
1939
2171
|
remove: args2["--remove"] === true,
|
|
1940
2172
|
log: (message) => log.info(message)
|
|
1941
2173
|
});
|
|
1942
2174
|
process.exit(0);
|
|
1943
2175
|
}
|
|
2176
|
+
if (command === "discover") {
|
|
2177
|
+
runDiscover({ log: (message) => log.info(message) });
|
|
2178
|
+
process.exit(0);
|
|
2179
|
+
}
|
|
1944
2180
|
if (command === "status") {
|
|
1945
2181
|
const args2 = parseArgs(rest);
|
|
1946
2182
|
const result = await runStatus({
|
package/lib/index.js
CHANGED
|
@@ -1337,7 +1337,7 @@ function readBody(req) {
|
|
|
1337
1337
|
|
|
1338
1338
|
// src/index.ts
|
|
1339
1339
|
var SERVER_NAME = "agent-comm-hub";
|
|
1340
|
-
var SERVER_VERSION = "0.
|
|
1340
|
+
var SERVER_VERSION = "0.4.0";
|
|
1341
1341
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1342
1342
|
var DEFAULT_PORT = 18764;
|
|
1343
1343
|
var DEFAULT_PATH = "/mcp";
|
package/lib/setup.js
CHANGED
|
@@ -1,13 +1,177 @@
|
|
|
1
1
|
// src/setup.ts
|
|
2
|
-
import { copyFile, mkdir, readFile,
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
4
|
+
import { homedir as homedir2 } from "node:os";
|
|
5
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/discover.ts
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readdirSync, accessSync, readFileSync, constants as fsConstants } from "node:fs";
|
|
4
11
|
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import { dirname, join, sep } from "node:path";
|
|
6
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
function registryFile() {
|
|
15
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "agents", "registry.json");
|
|
16
|
+
}
|
|
17
|
+
function expandHome(file, home) {
|
|
18
|
+
return file.startsWith("~/") ? join(home, file.slice(2)) : file;
|
|
19
|
+
}
|
|
20
|
+
function expandConfigFile(file, home) {
|
|
21
|
+
const expanded = expandHome(file, home);
|
|
22
|
+
const star = expanded.indexOf("*");
|
|
23
|
+
if (star < 0) return [expanded];
|
|
24
|
+
const prefix = expanded.slice(0, star);
|
|
25
|
+
const suffix = expanded.slice(star + 1);
|
|
26
|
+
const base = prefix.slice(0, prefix.lastIndexOf(sep));
|
|
27
|
+
if (!existsSync(base)) return [];
|
|
28
|
+
return readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(base, entry.name, suffix));
|
|
29
|
+
}
|
|
30
|
+
function validateRegistry(registry) {
|
|
31
|
+
if (!Array.isArray(registry.agents)) throw new Error('registry: missing "agents" array');
|
|
32
|
+
const ids = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const agent of registry.agents) {
|
|
34
|
+
if (typeof agent.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(agent.id)) {
|
|
35
|
+
throw new Error(`registry: bad agent id ${JSON.stringify(agent.id)}`);
|
|
36
|
+
}
|
|
37
|
+
if (ids.has(agent.id)) throw new Error(`registry: duplicate agent id '${agent.id}'`);
|
|
38
|
+
ids.add(agent.id);
|
|
39
|
+
if (!Array.isArray(agent.probe)) throw new Error(`registry: ${agent.id}: probe must be an array`);
|
|
40
|
+
if (agent.npm !== void 0 && !Array.isArray(agent.npm)) throw new Error(`registry: ${agent.id}: npm must be an array`);
|
|
41
|
+
if (!Array.isArray(agent.configs)) throw new Error(`registry: ${agent.id}: configs must be an array`);
|
|
42
|
+
for (const config of agent.configs) {
|
|
43
|
+
if (typeof config.file !== "string" || !config.file.startsWith("~/")) {
|
|
44
|
+
throw new Error(`registry: ${agent.id}: config file must be '~'-relative`);
|
|
45
|
+
}
|
|
46
|
+
if (config.file.split("/").includes("..")) {
|
|
47
|
+
throw new Error(`registry: ${agent.id}: config file must not contain '..'`);
|
|
48
|
+
}
|
|
49
|
+
if (config.file.split("*").length > 2) {
|
|
50
|
+
throw new Error(`registry: ${agent.id}: at most one '*' segment allowed`);
|
|
51
|
+
}
|
|
52
|
+
if (!["json", "toml", "dsh"].includes(config.strategy)) {
|
|
53
|
+
throw new Error(`registry: ${agent.id}: unknown strategy '${config.strategy}'`);
|
|
54
|
+
}
|
|
55
|
+
if (config.strategy === "json" && (typeof config.section !== "string" || config.entry === null)) {
|
|
56
|
+
throw new Error(`registry: ${agent.id}: json strategy needs a section and an entry`);
|
|
57
|
+
}
|
|
58
|
+
if (config.strategy !== "json" && config.entry !== null) {
|
|
59
|
+
throw new Error(`registry: ${agent.id}: only json strategy may carry an entry`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (agent.skill !== null && (typeof agent.skill !== "string" || !agent.skill.startsWith("~/"))) {
|
|
63
|
+
throw new Error(`registry: ${agent.id}: skill must be '~'-relative or null`);
|
|
64
|
+
}
|
|
65
|
+
if (agent.os !== void 0 && (!Array.isArray(agent.os) || agent.os.some((os) => !["win32", "darwin", "linux"].includes(os)))) {
|
|
66
|
+
throw new Error(`registry: ${agent.id}: invalid os list`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function loadRegistry(file = registryFile()) {
|
|
71
|
+
const registry = JSON.parse(readFileSync(file, "utf8"));
|
|
72
|
+
validateRegistry(registry);
|
|
73
|
+
return registry;
|
|
74
|
+
}
|
|
75
|
+
function commandOnPath(command, pathEnv, pathext, platform) {
|
|
76
|
+
const dirs = pathEnv.split(platform === "win32" ? ";" : ":");
|
|
77
|
+
const extensions = platform === "win32" ? ["", ...(pathext || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)] : [""];
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
const base = dir === "" ? "." : dir;
|
|
80
|
+
for (const ext of extensions) {
|
|
81
|
+
const candidate = join(base, command + ext);
|
|
82
|
+
try {
|
|
83
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
function readNpmNames(root) {
|
|
92
|
+
const names = [];
|
|
93
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
94
|
+
if (!entry.isDirectory()) continue;
|
|
95
|
+
if (entry.name.startsWith("@")) {
|
|
96
|
+
const scopeDir = join(root, entry.name);
|
|
97
|
+
for (const sub of readdirSync(scopeDir, { withFileTypes: true })) {
|
|
98
|
+
if (sub.isDirectory()) names.push(sub.name);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
names.push(entry.name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return names;
|
|
105
|
+
}
|
|
106
|
+
function npmGlobalRoot(platform) {
|
|
107
|
+
try {
|
|
108
|
+
const out = execFileSync(platform === "win32" ? "npm.cmd" : "npm", ["root", "-g"], { encoding: "utf8", windowsHide: true }).trim();
|
|
109
|
+
return out || null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function npmFallbackRoots(home, platform) {
|
|
115
|
+
if (platform === "win32") {
|
|
116
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
117
|
+
return [join(appData, "npm", "node_modules")];
|
|
118
|
+
}
|
|
119
|
+
return [
|
|
120
|
+
"/usr/local/lib/node_modules",
|
|
121
|
+
"/usr/lib/node_modules",
|
|
122
|
+
...existsSync(join(home, ".nvm", "versions", "node")) ? readdirSync(join(home, ".nvm", "versions", "node")).map((dir) => join(home, ".nvm", "versions", "node", dir, "lib", "node_modules")) : []
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function discover(registry, options = {}) {
|
|
126
|
+
const home = options.homeDir ?? homedir();
|
|
127
|
+
const platform = options.platform ?? process.platform;
|
|
128
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
129
|
+
const pathext = options.pathext ?? process.env.PATHEXT ?? "";
|
|
130
|
+
let npmNames = null;
|
|
131
|
+
if (!options.noNpm) {
|
|
132
|
+
if (options.npmRoot !== void 0 && options.npmRoot !== null) {
|
|
133
|
+
npmNames = existsSync(options.npmRoot) ? readNpmNames(options.npmRoot) : [];
|
|
134
|
+
} else if (options.npmRoot !== null) {
|
|
135
|
+
const root = npmGlobalRoot(platform);
|
|
136
|
+
if (root && existsSync(root)) {
|
|
137
|
+
npmNames = readNpmNames(root);
|
|
138
|
+
} else {
|
|
139
|
+
npmNames = [];
|
|
140
|
+
for (const fallback of npmFallbackRoots(home, platform)) {
|
|
141
|
+
if (existsSync(fallback)) {
|
|
142
|
+
npmNames = readNpmNames(fallback);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return registry.agents.filter((agent) => agent.os === void 0 || agent.os.includes(platform)).map((agent) => {
|
|
150
|
+
let source = "none";
|
|
151
|
+
const configFiles = [];
|
|
152
|
+
for (const config of agent.configs) {
|
|
153
|
+
for (const file of expandConfigFile(config.file, home)) {
|
|
154
|
+
if (existsSync(file)) {
|
|
155
|
+
configFiles.push(file);
|
|
156
|
+
if (source === "none") source = "config";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (source === "none" && agent.probe.some((command) => commandOnPath(command, pathEnv, pathext, platform))) {
|
|
161
|
+
source = "path";
|
|
162
|
+
}
|
|
163
|
+
if (source === "none" && npmNames !== null && (agent.probe.some((command) => npmNames.includes(command)) || (agent.npm ?? []).some((name) => npmNames.includes(name)))) {
|
|
164
|
+
source = "npm";
|
|
165
|
+
}
|
|
166
|
+
return { id: agent.id, source, configFiles, present: source !== "none" };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/setup.ts
|
|
7
171
|
var DEFAULT_URL = "http://127.0.0.1:18764/mcp";
|
|
8
172
|
var DEFAULT_SERVER = "agent-hub";
|
|
9
173
|
function defaultSkillSrc() {
|
|
10
|
-
return
|
|
174
|
+
return join2(dirname2(fileURLToPath2(import.meta.url)), "..", "agents", "SKILL.md");
|
|
11
175
|
}
|
|
12
176
|
function stamp() {
|
|
13
177
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -15,7 +179,7 @@ function stamp() {
|
|
|
15
179
|
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
16
180
|
}
|
|
17
181
|
async function readJson(file) {
|
|
18
|
-
if (!
|
|
182
|
+
if (!existsSync2(file)) return null;
|
|
19
183
|
try {
|
|
20
184
|
return JSON.parse(await readFile(file, "utf8"));
|
|
21
185
|
} catch (error) {
|
|
@@ -23,7 +187,7 @@ async function readJson(file) {
|
|
|
23
187
|
}
|
|
24
188
|
}
|
|
25
189
|
async function writeJsonNoBom(file, doc) {
|
|
26
|
-
await mkdir(
|
|
190
|
+
await mkdir(dirname2(file), { recursive: true });
|
|
27
191
|
await writeFile(file, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
28
192
|
}
|
|
29
193
|
async function backup(file) {
|
|
@@ -44,7 +208,7 @@ function resolveSection(doc, section) {
|
|
|
44
208
|
return node;
|
|
45
209
|
}
|
|
46
210
|
async function mergeJsonServer(file, section, entry, opts) {
|
|
47
|
-
if (!
|
|
211
|
+
if (!existsSync2(file)) return "skipped";
|
|
48
212
|
const doc = await readJson(file);
|
|
49
213
|
if (doc === null) return "skipped";
|
|
50
214
|
const servers = resolveSection(doc, section);
|
|
@@ -66,7 +230,7 @@ async function mergeJsonServer(file, section, entry, opts) {
|
|
|
66
230
|
return "changed";
|
|
67
231
|
}
|
|
68
232
|
async function mergeTomlSection(file, opts) {
|
|
69
|
-
if (!
|
|
233
|
+
if (!existsSync2(file)) return "skipped";
|
|
70
234
|
const text = await readFile(file, "utf8");
|
|
71
235
|
const marker = `[mcp_servers.${opts.serverName}]`;
|
|
72
236
|
const markerRe = new RegExp(`^\\[mcp_servers\\.${escapeRegExp(opts.serverName)}\\]`, "m");
|
|
@@ -104,7 +268,7 @@ ${DSH_PATCH_MARKER} (installed by \`agent-comm-hub setup\`; undo with \`setup --
|
|
|
104
268
|
`;
|
|
105
269
|
}
|
|
106
270
|
async function mergeDshPatch(file, opts) {
|
|
107
|
-
if (!
|
|
271
|
+
if (!existsSync2(file)) return "skipped";
|
|
108
272
|
const text = await readFile(file, "utf8");
|
|
109
273
|
const lines = text.split("\n");
|
|
110
274
|
const markerLine = lines.findIndex((line) => line.includes(DSH_PATCH_MARKER));
|
|
@@ -154,29 +318,40 @@ async function mergeDshPatch(file, opts) {
|
|
|
154
318
|
}
|
|
155
319
|
async function syncSkill(skillDir, skillSrc, remove, log) {
|
|
156
320
|
if (remove) {
|
|
157
|
-
if (
|
|
158
|
-
await mkdir(
|
|
321
|
+
if (existsSync2(skillDir)) {
|
|
322
|
+
await mkdir(dirname2(skillDir), { recursive: true });
|
|
159
323
|
await rmRecursive(skillDir);
|
|
160
324
|
log(` skill removed: ${skillDir}`);
|
|
161
325
|
}
|
|
162
326
|
return;
|
|
163
327
|
}
|
|
164
|
-
if (!
|
|
328
|
+
if (!existsSync2(skillSrc)) {
|
|
165
329
|
log(` SKILL.md source missing: ${skillSrc} (skipped)`);
|
|
166
330
|
return;
|
|
167
331
|
}
|
|
168
332
|
await mkdir(skillDir, { recursive: true });
|
|
169
|
-
await copyFile(skillSrc,
|
|
170
|
-
log(` skill -> ${
|
|
333
|
+
await copyFile(skillSrc, join2(skillDir, "SKILL.md"));
|
|
334
|
+
log(` skill -> ${join2(skillDir, "SKILL.md")}`);
|
|
171
335
|
}
|
|
172
336
|
async function rmRecursive(dir) {
|
|
173
337
|
const { rm } = await import("node:fs/promises");
|
|
174
338
|
await rm(dir, { recursive: true, force: true });
|
|
175
339
|
}
|
|
340
|
+
function substitute(entry, values) {
|
|
341
|
+
const out = {};
|
|
342
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
343
|
+
if (typeof value === "string") {
|
|
344
|
+
out[key] = value.replaceAll("{url}", values.url).replaceAll("{serverName}", values.serverName);
|
|
345
|
+
} else {
|
|
346
|
+
out[key] = value;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
176
351
|
async function runSetup(options = {}) {
|
|
177
352
|
const url = options.url ?? DEFAULT_URL;
|
|
178
353
|
const serverName = options.serverName ?? DEFAULT_SERVER;
|
|
179
|
-
const home = options.homeDir ??
|
|
354
|
+
const home = options.homeDir ?? homedir2();
|
|
180
355
|
const skillSrc = options.skillSrc ?? defaultSkillSrc();
|
|
181
356
|
const remove = options.remove === true;
|
|
182
357
|
const log = options.log ?? ((message) => console.log(message));
|
|
@@ -186,65 +361,40 @@ async function runSetup(options = {}) {
|
|
|
186
361
|
else if (status === "unchanged" || status === "absent") summary.unchanged.push(`${label}: ${file}`);
|
|
187
362
|
else if (status === "skipped") summary.skipped.push(`${label}: ${file}`);
|
|
188
363
|
};
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
record(status, target.label, target.file);
|
|
201
|
-
} catch (error) {
|
|
202
|
-
summary.errors.push(`${target.label}: ${target.file} \u2014 ${error.message}`);
|
|
203
|
-
log(` ${target.label}: SKIPPED \u2014 ${error.message}`);
|
|
364
|
+
const registry = loadRegistry();
|
|
365
|
+
const found = discover(registry, { homeDir: home, pathEnv: options.pathEnv, noNpm: options.noNpm === true });
|
|
366
|
+
const only = options.agent;
|
|
367
|
+
let targetAgents = [];
|
|
368
|
+
if (only !== void 0) {
|
|
369
|
+
const match = registry.agents.find((agent) => agent.id === only);
|
|
370
|
+
if (match === void 0) {
|
|
371
|
+
log(`agent '${only}' is not in the registry (see agents/registry.json)`);
|
|
372
|
+
} else {
|
|
373
|
+
targetAgents = [match];
|
|
374
|
+
log(`configure only: ${only}`);
|
|
204
375
|
}
|
|
376
|
+
} else {
|
|
377
|
+
targetAgents = found.filter((agent) => agent.present).map((agent) => registry.agents.find((entry) => entry.id === agent.id));
|
|
378
|
+
const present = targetAgents.map((agent) => agent.id);
|
|
379
|
+
log(`discovered: ${present.length > 0 ? present.join(", ") : "none"}`);
|
|
205
380
|
}
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let entries = [];
|
|
217
|
-
try {
|
|
218
|
-
entries = await readdir(dshProfilesDir, { withFileTypes: true });
|
|
219
|
-
} catch (error) {
|
|
220
|
-
summary.errors.push(`dsh profiles scan \u2014 ${error.message}`);
|
|
221
|
-
}
|
|
222
|
-
for (const entry of entries) {
|
|
223
|
-
if (!entry.isDirectory()) continue;
|
|
224
|
-
const patch = join(dshProfilesDir, entry.name, "cordis.patch.yml");
|
|
225
|
-
try {
|
|
226
|
-
const status = await mergeDshPatch(patch, { serverName, url, remove });
|
|
227
|
-
record(status, `dsh (${entry.name})`, patch);
|
|
228
|
-
} catch (error) {
|
|
229
|
-
summary.errors.push(`dsh (${entry.name}): ${patch} \u2014 ${error.message}`);
|
|
230
|
-
log(` dsh (${entry.name}): SKIPPED \u2014 ${error.message}`);
|
|
381
|
+
for (const agent of targetAgents) {
|
|
382
|
+
for (const config of agent.configs) {
|
|
383
|
+
for (const file of expandConfigFile(config.file, home)) {
|
|
384
|
+
try {
|
|
385
|
+
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 });
|
|
386
|
+
record(status, agent.id, file);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
summary.errors.push(`${agent.id}: ${file} \u2014 ${error.message}`);
|
|
389
|
+
log(` ${agent.id}: SKIPPED \u2014 ${error.message}`);
|
|
390
|
+
}
|
|
231
391
|
}
|
|
232
392
|
}
|
|
233
393
|
}
|
|
234
|
-
const skillDirs = [
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
join(home, ".config", "opencode", "skills", serverName),
|
|
239
|
-
join(home, ".kimi-code", "skills", serverName),
|
|
240
|
-
join(home, ".gemini", "skills", serverName),
|
|
241
|
-
join(home, ".codex", "skills", serverName),
|
|
242
|
-
join(home, ".zcode", "skills", serverName),
|
|
243
|
-
join(home, ".claude", "skills", serverName),
|
|
244
|
-
// config is manual; skill still useful
|
|
245
|
-
join(home, ".dsh", "skills", serverName)
|
|
246
|
-
// DSH skill (config auto-installed)
|
|
247
|
-
];
|
|
394
|
+
const skillDirs = [join2(home, ".agents", "skills", serverName)];
|
|
395
|
+
for (const agent of targetAgents) {
|
|
396
|
+
if (agent.skill !== null) skillDirs.push(join2(expandHome(agent.skill, home), serverName));
|
|
397
|
+
}
|
|
248
398
|
for (const dir of skillDirs) {
|
|
249
399
|
try {
|
|
250
400
|
await syncSkill(dir, skillSrc, remove, log);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-comm-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
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
|
-
"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",
|
|
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",
|
|
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",
|
|
42
42
|
"pack": "pnpm run build && pnpm pack",
|
|
43
43
|
"prepublishOnly": "pnpm run test && pnpm run build",
|
|
44
44
|
"prepare": "pnpm run build"
|