agent-comm-hub 0.2.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 +415 -361
- package/README.zh.md +289 -250
- package/agents/registry.json +201 -0
- package/lib/cli.js +1044 -83
- package/lib/index.js +642 -4
- package/lib/setup.js +284 -50
- package/package.json +3 -3
package/lib/setup.js
CHANGED
|
@@ -1,13 +1,177 @@
|
|
|
1
1
|
// src/setup.ts
|
|
2
2
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
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");
|
|
@@ -90,31 +254,104 @@ url = "${opts.url}"
|
|
|
90
254
|
function escapeRegExp(value) {
|
|
91
255
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
92
256
|
}
|
|
257
|
+
var DSH_PATCH_MARKER = "# \u2500\u2500 agent-comm-hub MCP client";
|
|
258
|
+
function dshPatchBlock(url, serverName) {
|
|
259
|
+
return `
|
|
260
|
+
${DSH_PATCH_MARKER} (installed by \`agent-comm-hub setup\`; undo with \`setup --remove\`) \u2500
|
|
261
|
+
- insert:
|
|
262
|
+
- id: ${serverName}
|
|
263
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
264
|
+
config:
|
|
265
|
+
serverName: ${serverName}
|
|
266
|
+
transport: streamable-http
|
|
267
|
+
url: ${url}
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
async function mergeDshPatch(file, opts) {
|
|
271
|
+
if (!existsSync2(file)) return "skipped";
|
|
272
|
+
const text = await readFile(file, "utf8");
|
|
273
|
+
const lines = text.split("\n");
|
|
274
|
+
const markerLine = lines.findIndex((line) => line.includes(DSH_PATCH_MARKER));
|
|
275
|
+
const hasBlock = markerLine >= 0;
|
|
276
|
+
const blockRange = () => {
|
|
277
|
+
let entryStart = -1;
|
|
278
|
+
for (let i = markerLine + 1; i < lines.length; i++) {
|
|
279
|
+
if (/^- /.test(lines[i])) {
|
|
280
|
+
entryStart = i;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let end = lines.length;
|
|
285
|
+
if (entryStart >= 0) {
|
|
286
|
+
for (let i = entryStart + 1; i < lines.length; i++) {
|
|
287
|
+
if (/^- /.test(lines[i])) {
|
|
288
|
+
end = i;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
let start = markerLine;
|
|
294
|
+
while (start > 0 && lines[start - 1].trim() === "") start--;
|
|
295
|
+
return { start, end };
|
|
296
|
+
};
|
|
297
|
+
const withoutBlock = () => {
|
|
298
|
+
const { start, end } = blockRange();
|
|
299
|
+
return lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
300
|
+
};
|
|
301
|
+
if (opts.remove) {
|
|
302
|
+
if (!hasBlock) return "absent";
|
|
303
|
+
const cleaned = withoutBlock();
|
|
304
|
+
await backup(file);
|
|
305
|
+
await writeFile(file, cleaned, "utf8");
|
|
306
|
+
return "removed";
|
|
307
|
+
}
|
|
308
|
+
if (hasBlock) {
|
|
309
|
+
if (lines.some((line) => line.includes(`url: ${opts.url}`))) return "unchanged";
|
|
310
|
+
const replaced = withoutBlock();
|
|
311
|
+
await backup(file);
|
|
312
|
+
await writeFile(file, replaced.trimEnd() + dshPatchBlock(opts.url, opts.serverName), "utf8");
|
|
313
|
+
return "changed";
|
|
314
|
+
}
|
|
315
|
+
await backup(file);
|
|
316
|
+
await writeFile(file, text.trimEnd() + dshPatchBlock(opts.url, opts.serverName), "utf8");
|
|
317
|
+
return "changed";
|
|
318
|
+
}
|
|
93
319
|
async function syncSkill(skillDir, skillSrc, remove, log) {
|
|
94
320
|
if (remove) {
|
|
95
|
-
if (
|
|
96
|
-
await mkdir(
|
|
321
|
+
if (existsSync2(skillDir)) {
|
|
322
|
+
await mkdir(dirname2(skillDir), { recursive: true });
|
|
97
323
|
await rmRecursive(skillDir);
|
|
98
324
|
log(` skill removed: ${skillDir}`);
|
|
99
325
|
}
|
|
100
326
|
return;
|
|
101
327
|
}
|
|
102
|
-
if (!
|
|
328
|
+
if (!existsSync2(skillSrc)) {
|
|
103
329
|
log(` SKILL.md source missing: ${skillSrc} (skipped)`);
|
|
104
330
|
return;
|
|
105
331
|
}
|
|
106
332
|
await mkdir(skillDir, { recursive: true });
|
|
107
|
-
await copyFile(skillSrc,
|
|
108
|
-
log(` skill -> ${
|
|
333
|
+
await copyFile(skillSrc, join2(skillDir, "SKILL.md"));
|
|
334
|
+
log(` skill -> ${join2(skillDir, "SKILL.md")}`);
|
|
109
335
|
}
|
|
110
336
|
async function rmRecursive(dir) {
|
|
111
337
|
const { rm } = await import("node:fs/promises");
|
|
112
338
|
await rm(dir, { recursive: true, force: true });
|
|
113
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
|
+
}
|
|
114
351
|
async function runSetup(options = {}) {
|
|
115
352
|
const url = options.url ?? DEFAULT_URL;
|
|
116
353
|
const serverName = options.serverName ?? DEFAULT_SERVER;
|
|
117
|
-
const home = options.homeDir ??
|
|
354
|
+
const home = options.homeDir ?? homedir2();
|
|
118
355
|
const skillSrc = options.skillSrc ?? defaultSkillSrc();
|
|
119
356
|
const remove = options.remove === true;
|
|
120
357
|
const log = options.log ?? ((message) => console.log(message));
|
|
@@ -124,43 +361,40 @@ async function runSetup(options = {}) {
|
|
|
124
361
|
else if (status === "unchanged" || status === "absent") summary.unchanged.push(`${label}: ${file}`);
|
|
125
362
|
else if (status === "skipped") summary.skipped.push(`${label}: ${file}`);
|
|
126
363
|
};
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
record(status, target.label, target.file);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
summary.errors.push(`${target.label}: ${target.file} \u2014 ${error.message}`);
|
|
141
|
-
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}`);
|
|
142
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"}`);
|
|
380
|
+
}
|
|
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
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
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));
|
|
143
397
|
}
|
|
144
|
-
const codexFile = join(home, ".codex", "config.toml");
|
|
145
|
-
try {
|
|
146
|
-
const status = await mergeTomlSection(codexFile, { serverName, url, remove });
|
|
147
|
-
record(status, "codex", codexFile);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
summary.errors.push(`codex: ${codexFile} \u2014 ${error.message}`);
|
|
150
|
-
log(` codex: SKIPPED \u2014 ${error.message}`);
|
|
151
|
-
}
|
|
152
|
-
const skillDirs = [
|
|
153
|
-
join(home, ".agents", "skills", serverName),
|
|
154
|
-
// cross-agent standard
|
|
155
|
-
join(home, ".minimax", "skills", serverName),
|
|
156
|
-
join(home, ".config", "opencode", "skills", serverName),
|
|
157
|
-
join(home, ".kimi-code", "skills", serverName),
|
|
158
|
-
join(home, ".gemini", "skills", serverName),
|
|
159
|
-
join(home, ".codex", "skills", serverName),
|
|
160
|
-
join(home, ".zcode", "skills", serverName),
|
|
161
|
-
join(home, ".claude", "skills", serverName)
|
|
162
|
-
// config is manual; skill still useful
|
|
163
|
-
];
|
|
164
398
|
for (const dir of skillDirs) {
|
|
165
399
|
try {
|
|
166
400
|
await syncSkill(dir, skillSrc, remove, log);
|
|
@@ -169,8 +403,8 @@ async function runSetup(options = {}) {
|
|
|
169
403
|
log(` skill ${dir}: SKIPPED \u2014 ${error.message}`);
|
|
170
404
|
}
|
|
171
405
|
}
|
|
172
|
-
if (remove) log("done. Manual
|
|
173
|
-
else log("done. Manual
|
|
406
|
+
if (remove) log("done. Manual target (see agents/README.md): Claude Code (.mcp.json).");
|
|
407
|
+
else log("done. Manual target (see agents/README.md): Claude Code (.mcp.json). Restart agent sessions (and the dsh profile) to pick up the MCP server.");
|
|
174
408
|
return summary;
|
|
175
409
|
}
|
|
176
410
|
export {
|
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",
|
|
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",
|
|
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"
|