agent-comm-hub 0.2.0 → 0.3.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/lib/setup.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/setup.ts
2
- import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
@@ -90,6 +90,68 @@ url = "${opts.url}"
90
90
  function escapeRegExp(value) {
91
91
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
92
  }
93
+ var DSH_PATCH_MARKER = "# \u2500\u2500 agent-comm-hub MCP client";
94
+ function dshPatchBlock(url, serverName) {
95
+ return `
96
+ ${DSH_PATCH_MARKER} (installed by \`agent-comm-hub setup\`; undo with \`setup --remove\`) \u2500
97
+ - insert:
98
+ - id: ${serverName}
99
+ name: '@deepseek-ai/dsh-mcp-client'
100
+ config:
101
+ serverName: ${serverName}
102
+ transport: streamable-http
103
+ url: ${url}
104
+ `;
105
+ }
106
+ async function mergeDshPatch(file, opts) {
107
+ if (!existsSync(file)) return "skipped";
108
+ const text = await readFile(file, "utf8");
109
+ const lines = text.split("\n");
110
+ const markerLine = lines.findIndex((line) => line.includes(DSH_PATCH_MARKER));
111
+ const hasBlock = markerLine >= 0;
112
+ const blockRange = () => {
113
+ let entryStart = -1;
114
+ for (let i = markerLine + 1; i < lines.length; i++) {
115
+ if (/^- /.test(lines[i])) {
116
+ entryStart = i;
117
+ break;
118
+ }
119
+ }
120
+ let end = lines.length;
121
+ if (entryStart >= 0) {
122
+ for (let i = entryStart + 1; i < lines.length; i++) {
123
+ if (/^- /.test(lines[i])) {
124
+ end = i;
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ let start = markerLine;
130
+ while (start > 0 && lines[start - 1].trim() === "") start--;
131
+ return { start, end };
132
+ };
133
+ const withoutBlock = () => {
134
+ const { start, end } = blockRange();
135
+ return lines.slice(0, start).concat(lines.slice(end)).join("\n");
136
+ };
137
+ if (opts.remove) {
138
+ if (!hasBlock) return "absent";
139
+ const cleaned = withoutBlock();
140
+ await backup(file);
141
+ await writeFile(file, cleaned, "utf8");
142
+ return "removed";
143
+ }
144
+ if (hasBlock) {
145
+ if (lines.some((line) => line.includes(`url: ${opts.url}`))) return "unchanged";
146
+ const replaced = withoutBlock();
147
+ await backup(file);
148
+ await writeFile(file, replaced.trimEnd() + dshPatchBlock(opts.url, opts.serverName), "utf8");
149
+ return "changed";
150
+ }
151
+ await backup(file);
152
+ await writeFile(file, text.trimEnd() + dshPatchBlock(opts.url, opts.serverName), "utf8");
153
+ return "changed";
154
+ }
93
155
  async function syncSkill(skillDir, skillSrc, remove, log) {
94
156
  if (remove) {
95
157
  if (existsSync(skillDir)) {
@@ -149,6 +211,26 @@ async function runSetup(options = {}) {
149
211
  summary.errors.push(`codex: ${codexFile} \u2014 ${error.message}`);
150
212
  log(` codex: SKIPPED \u2014 ${error.message}`);
151
213
  }
214
+ const dshProfilesDir = join(home, ".dsh", "profiles");
215
+ if (existsSync(dshProfilesDir)) {
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}`);
231
+ }
232
+ }
233
+ }
152
234
  const skillDirs = [
153
235
  join(home, ".agents", "skills", serverName),
154
236
  // cross-agent standard
@@ -158,8 +240,10 @@ async function runSetup(options = {}) {
158
240
  join(home, ".gemini", "skills", serverName),
159
241
  join(home, ".codex", "skills", serverName),
160
242
  join(home, ".zcode", "skills", serverName),
161
- join(home, ".claude", "skills", serverName)
243
+ join(home, ".claude", "skills", serverName),
162
244
  // config is manual; skill still useful
245
+ join(home, ".dsh", "skills", serverName)
246
+ // DSH skill (config auto-installed)
163
247
  ];
164
248
  for (const dir of skillDirs) {
165
249
  try {
@@ -169,8 +253,8 @@ async function runSetup(options = {}) {
169
253
  log(` skill ${dir}: SKIPPED \u2014 ${error.message}`);
170
254
  }
171
255
  }
172
- if (remove) log("done. Manual targets (see agents/README.md): Claude Code (.mcp.json), DSH (cordis.patch.yml).");
173
- 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.");
256
+ if (remove) log("done. Manual target (see agents/README.md): Claude Code (.mcp.json).");
257
+ 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
258
  return summary;
175
259
  }
176
260
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-comm-hub",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
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",
42
42
  "pack": "pnpm run build && pnpm pack",
43
43
  "prepublishOnly": "pnpm run test && pnpm run build",
44
44
  "prepare": "pnpm run build"