agent-comm-hub 0.9.0 → 0.10.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 CHANGED
@@ -98,7 +98,7 @@ itself).
98
98
 
99
99
  ```bash
100
100
  agent-comm-hub setup
101
- # or: agents/install-all.ps1 (PowerShell equivalent)
101
+ # (agents/install-all.ps1 is deprecated — it forwards to this command)
102
102
  # undo: agent-comm-hub setup --remove
103
103
  # list what is installed (no changes): agent-comm-hub discover
104
104
  # configure a single agent: agent-comm-hub setup --agent codex
@@ -131,7 +131,7 @@ curl -X POST http://127.0.0.1:18764/mcp \
131
131
 
132
132
  Each agent gets **one MCP server entry** pointing at `http://127.0.0.1:18764/mcp`, plus the shared English skill (`agents/SKILL.md`) that teaches it when and how to use the bridge tools. Templates live in [`agents/`](agents/README.md).
133
133
 
134
- **One-shot incremental sync** (recommended): `agents/install-all.ps1` merges the
134
+ **One-shot incremental sync** (recommended): `agent-comm-hub setup` merges the
135
135
  `agent-hub` entry into every installed agent's MCP config (mcode, opencode,
136
136
  Kimi Code, Gemini CLI, Codex, zcode, DSH) and installs the skill — it only
137
137
  touches the `agent-hub` key, backs up each file, and is idempotent. Claude Code
@@ -252,16 +252,31 @@ with `agent-comm-hub setup --remove` (or only refresh skills via
252
252
 
253
253
  | Tool | Purpose |
254
254
  |---|---|
255
- | `bridge_register(peerId)` | Claim or rename your identity (auto-registered at connect with the client name; optional for a readable id like `opencode:myproject`) |
256
- | `bridge_unregister()` | Leave the hub (removes peer, queue, and session binding; stays off until an explicit register) |
257
- | `bridge_chat(to, message)` | Send a chat message; `to: "all"` broadcasts |
258
- | `bridge_task(to, prompt, context?, deliverable?)` | Delegate a structured task |
259
- | `bridge_ack(ref, status, note?)` | Acknowledge a task (`accepted`/`rejected`/`done`/`failed`), routed back to the original sender |
260
- | `bridge_wait(from?, timeoutMs?)` | Long-poll for the next message (default 30 s) |
261
- | `bridge_poll(from?)` | Non-blocking drain of queued messages |
255
+ | `bridge_register(peerId, alias?)` | Claim or rename your identity (auto-registered at connect with the client name; optional for a readable id like `opencode:myproject`) |
256
+ | `bridge_rename(alias?, peer?, peerId?)` | Set your own display alias; renaming or true-renaming **another** peer needs manager rights |
257
+ | `bridge_unregister(peer?)` | Leave the hub (removes peer, queue, and session binding; stays off until an explicit register). With `peer`, kicks that peer (manager) |
258
+ | `bridge_chat(to, message, threadId?, priority?, ref?, expiresAt?)` | Send a chat message; `to: "all"` broadcasts. `expiresAt` is an absolute epoch-ms deadline: a message still undelivered after it is dropped and counted, not delivered late. `priority` (0\|1\|2, default 0) orders the recipient's mailbox |
259
+ | `bridge_notice(to, message, level?)` | Send a system-flavoured notice (`info`/`warn`/`error`) — read as a status, not as chat; creates no task |
260
+ | `bridge_task(to, prompt, context?, deliverable?, timeoutMs?, threadId?, ref?, expiresAt?)` | Delegate a structured task (`timeoutMs` is the assignee's SLA once it arrives; `expiresAt` is the delivery deadline) |
261
+ | `bridge_ack(ref, status, note?, progress?)` | Acknowledge a task (`accepted`/`working`/`rejected`/`done`/`failed`), routed back to the original sender |
262
+ | `bridge_wait(from?, ref?, threadId?, timeoutMs?, ack?, maxMessages?)` | Long-poll for the next message (default 30 s). `ack: "manual"` leases it instead of consuming it; `maxMessages > 1` returns `{type:"messages", messages:[…]}` in one call |
263
+ | `bridge_poll(from?, ack?)` | Non-blocking drain of queued messages (`ack: "manual"` leases them) |
264
+ | `bridge_commit(ids)` | Acknowledge leased messages; uncommitted ones come back when the lease expires |
265
+ | `bridge_nack(ids)` | Give leased (or dead-lettered) messages back immediately — use when handling failed |
262
266
  | `bridge_status()` | Hub health: peers with connected/queued/waiting state |
263
- | `bridge_peers()` | Who is online |
264
- | `bridge_history(peer?, limit?)` | Recent messages (context refresh after reconnect) |
267
+ | `bridge_peers()` | Who is online, how reachable (`online`/`idle`/`parked`) and how much is queued |
268
+ | `bridge_history(peer?, limit?, channel?)` | Recent messages (context refresh after reconnect) |
269
+ | `bridge_tasks(role?, status?, limit?, peer?)` | Task ledger rows you sent or received |
270
+ | `bridge_task_status(ref)` | One task's state, per-assignee rows and the full ack timeline (also carries the A2A aliases `taskId` / `contextId` / `a2aState`) |
271
+ | `bridge_task_cancel(ref, reason?)` | Cancel a task — either side may; routed to the other side and terminal |
272
+ | `bridge_task_input(ref, input)` | Answer an `input-required` interrupt; the task resumes to `working` |
273
+ | `bridge_announce(card)` | Say what you can do (skills, `acceptsTasks`, modes, runner) |
274
+ | `bridge_discover(skill?, tag?, acceptsTasks?, idleOnly?)` | Find a peer to hand work to, with its current load |
275
+ | `bridge_group_create` · `bridge_group_send` · `bridge_group_list` · `bridge_group_delete` · `bridge_group_add_member` · `bridge_group_remove_member` | Named channels for a subset of peers |
276
+
277
+ That is the full 26 message + group tools; the 11 control tools are below. (37 in total and symmetric on every side — see [ARCHITECTURE.md](ARCHITECTURE.md) §4.)
278
+
279
+ Two peer ids are reserved and cannot be registered or renamed into: `all` (broadcast) and `hub` (the hub's own drop notices — `bridge_wait { from: "hub" }` subscribes to them).
265
280
 
266
281
  ### herdr control tools (optional)
267
282
 
@@ -294,6 +309,7 @@ socket — physical input, read output:
294
309
  | `bridge_pane_send(target, text, enter?)` | Type text into a pane (slash commands execute; Enter submits by default) |
295
310
  | `bridge_pane_keys(target, keys)` | Raw key presses to any pane |
296
311
  | `bridge_pane_read(target, lines?, source?)` | Read a pane's recent output |
312
+ | `bridge_pane_wait(target, until?, timeoutMs?)` | Wait for a pane to reach a state (idle / working / blocked / done) |
297
313
 
298
314
  Verified live: a MiniMax Code session was driven end-to-end through the hub —
299
315
  prompt injected via `bridge_pane_send`, reply collected via
@@ -317,7 +333,17 @@ continuous. Renaming yourself is open to everyone; renaming/kicking
317
333
  history (`bridge_history { peer }` / `peer: "all"`) requires the **manager**
318
334
  role (`--manager-peers`, default `agent-hub-cli,hub-admin` — the desktop GUI
319
335
  and the web console identities).
320
- This is a convention on top of the loopback trust model, not authentication.
336
+
337
+ With no credential configured this is a convention on top of the loopback trust
338
+ model, not authentication — the peer id is simply whoever claimed it first. Set
339
+ `--manager-token <secret>` (or drop the secret in `<state-file dir>/manager.token`)
340
+ to make manager PRIVILEGES require it: rename/kick/full-history/task-board and the
341
+ SSE relay then need `Authorization: Bearer <secret>`, and a credential-less client
342
+ that claims a manager id gets no power. The id itself is not reserved — the
343
+ desktop GUI identifies itself as `agent-hub-cli`, so reserving it would stop the
344
+ GUI from connecting. Note that claiming the id still occupies its identity and
345
+ mailbox even without the credential, and that in remote mode (`--auth-tokens`) the
346
+ token table owns the `Authorization` header, so `--manager-token` is ignored there.
321
347
 
322
348
  Rosters persist: `--state-file` (default `~/.agent-comm-hub/roster.json`,
323
349
  `off` to disable) keeps aliases and client info across hub restarts.
@@ -347,12 +373,42 @@ agent-comm-hub service install|uninstall [options] one-shot auto-start
347
373
  --wait-timeout-ms <n> Long-poll ceiling for bridge_wait (default 60000)
348
374
  --default-wait-ms <n> bridge_wait default budget (default 30000)
349
375
  --connected-window-ms <n> Peer counts as active within this window (default 30000)
350
- --peer-idle-timeout-ms <n> Auto-unregister idle peers after this; 0 disables (default 600000)
376
+ --peer-idle-timeout-ms <n> Park (not delete) idle peers after this; 0 disables the GC
377
+ entirely (default 600000)
378
+ --parked-ttl-ms <n> Drop a parked mailbox after this; 0 disables expiry
379
+ (default 604800000 = 7 days)
380
+ --rate-per-min <n> Per-peer mailbox rate limit (messages per minute); 0 (default)
381
+ is unlimited. Guards a recipient against a broadcast storm;
382
+ excess is counted as `droppedRate` in the roster
383
+ --db <path> SQLite state file for history/mailboxes/roster/groups/tasks
384
+ (default ~/.agent-comm-hub/state.db; "off" = memory + JSON roster)
385
+ --auth-tokens <file> REMOTE MODE: require `Authorization: Bearer <token>`; the token,
386
+ not the client name, decides the peer id. Also refuses a
387
+ non-loopback --host unless this is set.
388
+ --allow-join (remote mode) accept token-less walk-in peers, each on a unique
389
+ id; LAN/VPN only — keep it off on the public internet.
390
+ --heartbeat-ms <n> SSE heartbeat cadence (default 20000)
391
+ --runner <json> Executor for a peer that is NOT polling: one JSON object
392
+ or an array, e.g. {"peer":"codex","kind":"cli",
393
+ "command":"codex","args":["exec","{prompt}"]} or
394
+ {"peer":"mcode","kind":"herdr"}. Off by default; the
395
+ runner's outcome is written back as that peer's ack.
351
396
  --herdr-bin <path> herdr CLI binary for bridge_agent_* control tools
352
397
  (default herdr, resolved via PATH)
353
398
  --herdr-timeout-ms <n> Default cap for one herdr call in ms (default 30000)
354
399
  --manager-peers <ids> Comma-separated roster managers, or "all"
355
400
  (default agent-hub-cli,hub-admin — desktop GUI + web console)
401
+ --manager-token <secret> Require this secret (Authorization: Bearer <secret>) for hub
402
+ manager PRIVILEGES (rename/kick/full history/relay); otherwise
403
+ <state-file dir>/manager.token is read when present. The peer
404
+ id itself is not reserved. Ignored with --auth-tokens.
405
+ --allow-origins <list> Comma-separated browser origins allowed to call the MCP
406
+ endpoint (e.g. https://hub.example.com). Requests without
407
+ an Origin header and loopback origins are always allowed;
408
+ anything else gets 403 (the MCP spec's DNS-rebinding rule).
409
+ --insecure Allow --host to bind a non-loopback address with NO token
410
+ table. Refused by default: without --auth-tokens the hub
411
+ would trust every client that can reach the port.
356
412
  --state-file <path> Roster persistence file
357
413
  (default ~/.agent-comm-hub/roster.json; "off" = memory only)
358
414
  --url <u> / --server-name <n> / --remove / --dry-run (setup/service/status)
@@ -430,7 +486,7 @@ const hub = startHub({ port: 18764 }, console) // returns { hub, registry, serve
430
486
  ```bash
431
487
  pnpm install
432
488
  pnpm typecheck # tsc --noEmit (strict)
433
- pnpm test # test suite (282 checks: 156 smoke + 37 setup + 11 ops + 35 herdr + 23 discover + 20 e2e)
489
+ pnpm test # test suite (314 checks: 169 smoke + 46 setup + 11 ops + 35 herdr + 33 discover + 20 e2e)
434
490
  npm run build:admin # admin/ workspace (npm) → single-file assets/admin.html
435
491
  pnpm run build # esbuild → lib/{cli,index,setup}.js (zero deps)
436
492
  pnpm pack # build + npm pack (publishing artifact)
@@ -481,4 +537,4 @@ Tests cover registration, duplicate rejection, chat routing, sender-filtered wai
481
537
 
482
538
  ## License
483
539
 
484
- MIT — see [LICENSE](LICENSE). Contributions welcome: keep the 282-check suite green (`pnpm test`) and zero runtime dependencies. Architecture: [ARCHITECTURE.md](ARCHITECTURE.md).
540
+ MIT — see [LICENSE](LICENSE). Contributions welcome: keep the 314-check suite green (`pnpm test`) and zero runtime dependencies. Architecture: [ARCHITECTURE.md](ARCHITECTURE.md).
package/README.zh.md CHANGED
@@ -93,7 +93,7 @@ agent-comm-hub status # hub 是否在跑?谁在线?
93
93
 
94
94
  ```bash
95
95
  agent-comm-hub setup
96
- # 或 PowerShell 版:agents/install-all.ps1
96
+ # (agents/install-all.ps1 已废弃,它只是转发到这个命令)
97
97
  # 卸载:agent-comm-hub setup --remove
98
98
  # 查看本机装了什么(只读):agent-comm-hub discover
99
99
  # 只配置某一个:agent-comm-hub setup --agent codex
@@ -276,7 +276,7 @@ const hub = startHub({ port: 18764 }, console) // 返回 { hub, registry, server
276
276
  ```bash
277
277
  pnpm install
278
278
  pnpm typecheck # tsc --noEmit(strict)
279
- pnpm test # 测试套件(282 项:156 冒烟 + 37 安装器 + 11 运维 + 35 herdr + 23 发现 + 20 e2e)
279
+ pnpm test # 测试套件(314 项:169 冒烟 + 46 安装器 + 11 运维 + 35 herdr + 33 发现 + 20 e2e)
280
280
  npm run build:admin # admin/ 工作区(用 npm)→ 单文件 assets/admin.html
281
281
  pnpm run build # esbuild → lib/{cli,index,setup}.js(零依赖)
282
282
  pnpm pack # 构建 + npm pack(发布产物)
package/agents/SKILL.md CHANGED
@@ -6,7 +6,8 @@ description: >-
6
6
  chat, sending messages or tasks to another agent, waiting for another
7
7
  agent's reply, or forwarding results between agents. Tools:
8
8
  bridge_register / bridge_chat / bridge_task / bridge_ack / bridge_wait /
9
- bridge_poll / bridge_status / bridge_peers / bridge_history.
9
+ bridge_poll / bridge_status / bridge_peers / bridge_history /
10
+ bridge_commit / bridge_nack.
10
11
  ---
11
12
 
12
13
  # agent-comm-hub usage
@@ -23,26 +24,59 @@ Optional: call `bridge_register(peerId)` to claim a readable id
23
24
 
24
25
  ## Tools
25
26
 
26
- - `bridge_chat(to, message, threadId?, ref?)` — send a chat to a peer, group
27
- id, or `"all"` (broadcast). `threadId` groups a multi-turn dialog; `ref`
28
- marks a reply-to.
29
- - `bridge_task(to, prompt, context?, deliverable?, timeoutMs?, threadId?)` —
27
+ - `bridge_chat(to, message, threadId?, ref?, expiresAt?)` — send a chat to a peer,
28
+ group id, or `"all"` (broadcast). `threadId` groups a multi-turn dialog; `ref`
29
+ marks a reply-to (chain depth is bounded — see Notes). `expiresAt` is an
30
+ absolute epoch-ms deadline: a message still undelivered when it passes is
31
+ dropped (counted as `droppedExpired`) instead of arriving late.
32
+ - `bridge_notice(to, message, level?)` — send a system-flavoured line
33
+ ("your mailbox dropped 3 messages", "hub restarting"). `level` is
34
+ `info` | `warn` | `error`. Use it instead of `bridge_chat` when the recipient
35
+ should read it as a status, not as a peer talking; it creates no task.
36
+ - `bridge_task(to, prompt, context?, deliverable?, timeoutMs?, threadId?, ref?, expiresAt?)` —
30
37
  delegate a structured task. `timeoutMs` is a soft SLA: the hub marks the
31
- task `timeout` if no terminal ack arrives in time.
38
+ task `timeout` if no terminal ack arrives in time. `ref` names the task this
39
+ one answers; a task answering a task is a re-delegation, and that chain is
40
+ capped at 5 hops.
32
41
  - `bridge_ack(ref, status, note?, progress?)` — acknowledge a task.
33
42
  Statuses: `accepted` | `working` | `rejected` | `done` | `failed`.
34
43
  `working` + `progress` (0–100) report intermediate state. Terminal
35
44
  statuses lock the task (later acks are recorded on the timeline but do
36
45
  not reopen it). The ack is auto-routed to the original sender of `ref`.
37
46
  - `bridge_task_status(ref)` — one task's ledger state, per-assignee state,
38
- and full ack timeline.
47
+ and full ack timeline (also carries the A2A aliases `taskId` / `contextId` /
48
+ `a2aState`).
39
49
  - `bridge_tasks(role?, status?, limit?)` — list tasks you sent or received.
40
- - `bridge_wait(from?, ref?, threadId?, timeoutMs?)` — long-poll for the next
50
+ - `bridge_task_cancel(ref, reason?)` — cancel a task. Either side may: the sender
51
+ stops work it no longer wants, an assignee reports it will not finish. Routed to
52
+ the other side; `canceled` is terminal and a later ack cannot reopen it.
53
+ - `bridge_task_input(ref, input)` — answer a task whose assignee acked
54
+ `input-required`. Only the SENDER calls this; the answer reaches the waiting
55
+ assignee and the task resumes to `working`.
56
+ - `bridge_announce(card)` — say what you can do: skills, whether you accept tasks,
57
+ how many at once, input/output modes, and how you are executed when not polling.
58
+ Only peers that set `acceptsTasks: true` are offered by `bridge_discover`.
59
+ - `bridge_discover(skill?, tag?, acceptsTasks?, idleOnly?)` — find a peer to hand
60
+ work to. Returns matches with their current load (`queued` / `waiting` /
61
+ `running`); by default only peers that accept tasks.
62
+ - `bridge_wait(from?, ref?, threadId?, timeoutMs?, ack?, maxMessages?)` — long-poll for the next
41
63
  message (default 30 s; `ref` waits for that task's ack; `threadId` narrows
42
- to one dialog). Loop it to hold a real-time conversation.
43
- - `bridge_poll(from?)` — non-blocking drain of every queued message.
64
+ to one dialog). Loop it to hold a real-time conversation. `ack: "manual"`
65
+ LEASES the message instead of consuming it.
66
+ - `bridge_poll(from?, ack?)` — non-blocking drain of every queued message.
67
+ - `bridge_commit(ids)` — confirm you have finished with leased messages. Until you
68
+ do, they are handed out again when the lease expires (with `attempts` growing),
69
+ so `ack: "manual"` + `bridge_commit` is the at-least-once path: use it when
70
+ losing a message would be worse than handling one twice. A message that keeps
71
+ being handed out without a commit is dead-lettered rather than looping forever
72
+ (`bridge_status` shows `inFlight` / `deadLettered`).
73
+ - `bridge_nack(ids)` — give leased messages back now (handling failed), or revive
74
+ a dead-lettered one.
44
75
  - `bridge_status()` / `bridge_peers()` — hub health and who is online.
45
- - `bridge_history(peer?, limit?, channel?)` — recent messages (newest first).
76
+ - `bridge_history(peer?, limit?, channel?, since?)` — recent messages (newest
77
+ first). `since` is a resume cursor: record `bridge_status().lastSeq`, then read
78
+ only what happened after it, with no repeats. A small `limit` still truncates,
79
+ so loop until you get an empty page.
46
80
  - `bridge_group_create/send/list/delete` + `add_member`/`remove_member` —
47
81
  named channels for a subset of peers.
48
82
 
@@ -104,4 +138,32 @@ Follow the scenario directly (tool names stay as-is):
104
138
  - Cadence: use 10–30 s short polls when the peer is active; check
105
139
  `bridge_peers()` when unsure.
106
140
  - Task terminal states (`done`/`failed`/`rejected`/`timeout`) cannot be
107
- reopened; start a new task instead of re-acking.
141
+ reopened; start a new task instead of re-acking.
142
+ - **Reply chains are counted, not enforced — nothing is ever refused.**
143
+ Every message carrying `ref` sits one hop deeper than the message it points at.
144
+ A plain conversation (chat / ack replies) counts up to **50** hops; a **task
145
+ re-delegation chain** — a task replying to a task — counts up to **5**. Past
146
+ either ceiling `hop` simply stops counting: the message and its `ref` are
147
+ delivered unchanged, so correlation never breaks and `bridge_wait({ ref })`
148
+ keeps working on arbitrarily long chains. Read `hop` as “how deep this chain
149
+ is, capped” — reaching the ceiling is a hint that the conversation has become
150
+ a loop, not an error to recover from.
151
+ - **Nothing is dropped silently any more.** If your mailbox overflows (`maxQueue`)
152
+ or a message hits its `expiresAt` before you read it, the hub counts it and
153
+ `bridge_status` / `bridge_peers` show `droppedOverflow` / `droppedExpired` on
154
+ your row. When you are waiting you may also receive a `notice` whose `from` is
155
+ the reserved id `hub` — `bridge_wait { from: "hub" }` subscribes to exactly
156
+ those. `all` and `hub` are reserved: you cannot register or rename into them.
157
+ - **Need at-least-once instead of best-effort?** Take messages with
158
+ `bridge_wait { ack: "manual" }` (or `bridge_poll { ack: "manual" }`) and call
159
+ `bridge_commit` once you have really handled them. Uncommitted messages come
160
+ back when the lease expires, with `attempts` incremented; `bridge_nack` gives
161
+ one back immediately. After too many hand-outs a message is dead-lettered
162
+ instead of looping (see `deadLettered` in the roster).
163
+ - **SSE pushes are a client-runtime event channel, not a wake-up.** A hub
164
+ notification arrives on the stream you subscribed to; it does NOT start a model
165
+ turn, and an agent that is not polling will not be woken by it. Waking an agent
166
+ that is not in a `bridge_wait` loop is the job of a *runner* — the hub runs the
167
+ configured executor for that peer and writes the outcome back as its ack
168
+ (`--runner`; see the hub README). If you want to be woken, either loop on
169
+ `bridge_wait` or ask the operator to configure a runner for you.
@@ -1,23 +1,19 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Sync the agent-comm-hub MCP entry into every installed agent's own config —
4
- incrementally. Only the "agent-hub" server key is added/updated; all other
5
- content is preserved byte-for-byte semantically, each file is backed up first,
6
- and re-running is a no-op. -Remove undoes everything.
3
+ DEPRECATED shim. Use `agent-comm-hub setup` instead.
7
4
 
8
- Covered agents (auto-skipped when not installed):
9
- - MiniMax Code (mcode) ~/.minimax/mcp.json + ~/.minimax/mcp/mcp.json
10
- - opencode ~/.config/opencode/opencode.json
11
- - Kimi Code ~/.kimi-code/mcp.json
12
- - Gemini CLI ~/.gemini/settings.json
13
- - Codex ~/.codex/config.toml (appends the section if missing)
14
- - zcode ~/.zcode/cli/config.json (mcp.servers)
5
+ .DESCRIPTION
6
+ This script used to carry its own copy of the agent list and the merge logic.
7
+ That copy drifted: it synced the skill into eight fixed directories with no
8
+ "is this agent installed?" check, so running it on a machine with two agents
9
+ created skill folders for six agents that were not there.
15
10
 
16
- Not covered (manual, documented in agents/README.md):
17
- - Claude Code -> project-level .mcp.json (copy agents/claude-code/.mcp.json)
18
- (~/.claude.json is not touched: it carries credentials and
19
- cannot be round-tripped safely)
20
- - DSH -> merge agents/dsh/cordis.patch.yml into the profile patch
11
+ `agent-comm-hub setup` is registry-driven (`agents/registry.json`), only
12
+ touches agents that are actually installed, backs every file up, and is
13
+ covered by the test suite. It is the supported path.
14
+
15
+ This file is kept so existing callers keep working: it forwards to that
16
+ command with the same three switches. It no longer contains an agent list.
21
17
 
22
18
  .EXAMPLE
23
19
  powershell -ExecutionPolicy Bypass -File install-all.ps1
@@ -30,154 +26,23 @@ param(
30
26
  )
31
27
 
32
28
  $ErrorActionPreference = 'Stop'
33
- $userHome = $env:USERPROFILE
34
- $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
35
- $skillSrc = Join-Path $PSScriptRoot 'SKILL.md'
36
-
37
- function Write-Step($text) { Write-Host "[agent-comm-hub] $text" -ForegroundColor Cyan }
38
-
39
- function Read-Json($path) {
40
- if (-not (Test-Path $path)) { return $null }
41
- try { return [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8) | ConvertFrom-Json }
42
- catch { throw "cannot parse JSON: $path ($($_.Exception.Message))" }
43
- }
44
-
45
- function Write-JsonNoBom($path, $value) {
46
- $dir = Split-Path $path
47
- if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
48
- [System.IO.File]::WriteAllText($path, ($value | ConvertTo-Json -Depth 50), $utf8NoBom)
49
- }
50
-
51
- function Backup($path) {
52
- $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
53
- Copy-Item $path "$path.bak-$stamp" -Force -ErrorAction SilentlyContinue
54
- return "$(Split-Path $path -Leaf).bak-$stamp"
55
- }
56
-
57
- function Sync-Skill($agentName, $skillDir) {
58
- if ($Remove) {
59
- if (Test-Path $skillDir) { Remove-Item $skillDir -Recurse -Force; Write-Step " ${agentName}: removed skill $skillDir" }
60
- return
61
- }
62
- if (-not (Test-Path $skillSrc)) { Write-Step " ${agentName}: SKILL.md source missing (skipped)"; return }
63
- New-Item -ItemType Directory -Path $skillDir -Force | Out-Null
64
- Copy-Item $skillSrc (Join-Path $skillDir 'SKILL.md') -Force
65
- Write-Step " ${agentName}: skill -> $skillDir\SKILL.md"
66
- }
67
29
 
68
- function Merge-JsonServer($label, $file, $sectionName, $entry) {
69
- if (-not (Test-Path $file)) { Write-Step " ${label}: not installed ($file missing, skipped)"; return }
70
- try {
71
- $doc = Read-Json $file
72
- if ($null -eq $doc) { Write-Step " ${label}: empty config (skipped)"; return }
73
- # Resolve (creating when missing) a dotted section path like 'mcp.servers'.
74
- $servers = $doc
75
- foreach ($part in $sectionName.Split('.')) {
76
- if ($null -eq $servers.$part) {
77
- $servers | Add-Member -NotePropertyName $part -NotePropertyValue ([ordered]@{}) -Force
78
- }
79
- $servers = $servers.$part
80
- }
81
- $changed = $false
82
- if ($servers.PSObject.Properties.Name -contains $ServerName) {
83
- $existing = $servers.$ServerName
84
- $changed = ($existing.url -ne ${Url})
85
- } else {
86
- $changed = $true
87
- }
88
- if ($Remove) {
89
- if ($servers.PSObject.Properties.Name -contains $ServerName) {
90
- $servers.PSObject.Properties.Remove($ServerName)
91
- $bak = Backup $file
92
- Write-JsonNoBom $file $doc
93
- Write-Step " ${label}: removed '$ServerName' from $file (backup: $bak)"
94
- } else {
95
- Write-Step " ${label}: '$ServerName' not present in $file"
96
- }
97
- return
98
- }
99
- if (-not $changed) { Write-Step " ${label}: '$ServerName' already set -> ${Url} ($file unchanged)"; return }
100
- $bak = Backup $file
101
- $servers | Add-Member -NotePropertyName $ServerName -NotePropertyValue $entry -Force
102
- Write-JsonNoBom $file $doc
103
- Write-Step " ${label}: merged '$ServerName' -> ${Url} into $file (backup: $bak)"
104
- } catch {
105
- Write-Step " ${label}: SKIPPED — $($_.Exception.Message)"
106
- }
107
- }
30
+ Write-Warning 'install-all.ps1 is deprecated; use `agent-comm-hub setup` (this forwards to it).'
108
31
 
109
- if ($Remove) { Write-Step 'uninstalling from all agents...' } else { Write-Step "syncing '$ServerName' -> $Url to all agents..." }
32
+ # Prefer an installed CLI; fall back to this checkout's built bundle.
33
+ $cli = Get-Command agent-comm-hub -ErrorAction SilentlyContinue
34
+ $setupArgs = @('setup', '--url', $Url, '--server-name', $ServerName)
35
+ if ($Remove) { $setupArgs += '--remove' }
110
36
 
111
- # ---------- MiniMax Code (mcode): mcpServers at root of ~/.minimax/mcp.json + ~/.minimax/mcp/mcp.json
112
- $mcodeEntry = [ordered]@{
113
- url = $Url; type = 'streamable-http'; enabled = $true; configured = $true; timeout = 120000
114
- description = 'agent-comm-hub: talk to every other agent connected to the hub (bridge_register auto, bridge_chat/task/ack/wait/poll/status/peers/history).'
115
- }
116
- Write-Step '- mcode'
117
- foreach ($f in @((Join-Path $userHome '.minimax\mcp.json'), (Join-Path $userHome '.minimax\mcp\mcp.json'))) {
118
- Merge-JsonServer 'mcode' $f 'mcpServers' $mcodeEntry
37
+ if ($cli) {
38
+ & $cli.Source @setupArgs
39
+ exit $LASTEXITCODE
119
40
  }
120
41
 
121
- # ---------- opencode: mcp.<name> in ~/.config/opencode/opencode.json
122
- $opencodeEntry = [ordered]@{ type = 'remote'; url = $Url; enabled = $true }
123
- Write-Step '- opencode'
124
- Merge-JsonServer 'opencode' (Join-Path $userHome '.config\opencode\opencode.json') 'mcp' $opencodeEntry
125
-
126
- # ---------- Kimi Code: mcpServers at root of ~/.kimi-code/mcp.json
127
- $kimiEntry = [ordered]@{ transport = 'http'; url = $Url; startupTimeoutMs = 30000; toolTimeoutMs = 120000 }
128
- Write-Step '- kimi-code'
129
- Merge-JsonServer 'kimi-code' (Join-Path $userHome '.kimi-code\mcp.json') 'mcpServers' $kimiEntry
130
-
131
- # ---------- Gemini CLI: mcpServers in ~/.gemini/settings.json
132
- $geminiEntry = [ordered]@{ type = 'http'; url = $Url }
133
- Write-Step '- gemini-cli'
134
- Merge-JsonServer 'gemini-cli' (Join-Path $userHome '.gemini\settings.json') 'mcpServers' $geminiEntry
135
-
136
- # ---------- Codex: append [mcp_servers.<name>] to ~/.codex/config.toml (incremental append only)
137
- $codexFile = Join-Path $userHome '.codex\config.toml'
138
- Write-Step '- codex'
139
- if (Test-Path $codexFile) {
140
- $codexText = [System.IO.File]::ReadAllText($codexFile, [System.Text.Encoding]::UTF8)
141
- $section = "[mcp_servers.$ServerName]"
142
- if ($Remove) {
143
- if ($codexText -match "(?m)^\[mcp_servers\.$([regex]::Escape($ServerName))\]") {
144
- $bak = Backup $codexFile
145
- $clean = [regex]::Replace($codexText, "(?m)^\[mcp_servers\.$([regex]::Escape($ServerName))\][^\r\n]*(\r?\n(?!\[).*)*(\r?\n)?", '')
146
- [System.IO.File]::WriteAllText($codexFile, $clean, $utf8NoBom)
147
- Write-Step " codex: removed '$ServerName' section from $codexFile (backup: $bak)"
148
- } else {
149
- Write-Step " codex: '$ServerName' section not present in $codexFile"
150
- }
151
- } elseif ($codexText -match "(?m)^\[mcp_servers\.$([regex]::Escape($ServerName))\]") {
152
- Write-Step " codex: '$ServerName' section already present ($codexFile unchanged)"
153
- } else {
154
- $bak = Backup $codexFile
155
- $block = "`n[mcp_servers.$ServerName]`ntype = `"streamable-http`"`nurl = `"$Url`"`n"
156
- [System.IO.File]::WriteAllText($codexFile, $codexText.TrimEnd() + $block, $utf8NoBom)
157
- Write-Step " codex: appended '$ServerName' section to $codexFile (backup: $bak)"
158
- }
159
- } else {
160
- Write-Step " codex: not installed ($codexFile missing, skipped)"
42
+ $local = Join-Path (Split-Path -Parent $PSScriptRoot) 'lib/cli.js'
43
+ if (Test-Path $local) {
44
+ & node $local @setupArgs
45
+ exit $LASTEXITCODE
161
46
  }
162
47
 
163
- # ---------- zcode: mcp.servers in ~/.zcode/cli/config.json
164
- $zcodeEntry = [ordered]@{ enabled = $true; type = 'remote'; url = $Url }
165
- Write-Step '- zcode'
166
- Merge-JsonServer 'zcode' (Join-Path $userHome '.zcode\cli\config.json') 'mcp.servers' $zcodeEntry
167
- Sync-Skill 'zcode' (Join-Path $userHome ".zcode\skills\$ServerName")
168
-
169
- # ---------- skills: cross-agent standard location + each agent's private dir
170
- $skillDirs = @(
171
- (Join-Path $userHome ".agents\skills\$ServerName"), # cross-agent standard
172
- (Join-Path $userHome ".minimax\skills\$ServerName"),
173
- (Join-Path $userHome ".config\opencode\skills\$ServerName"),
174
- (Join-Path $userHome ".kimi-code\skills\$ServerName"),
175
- (Join-Path $userHome ".gemini\skills\$ServerName"),
176
- (Join-Path $userHome ".codex\skills\$ServerName"),
177
- (Join-Path $userHome ".zcode\skills\$ServerName"),
178
- (Join-Path $userHome ".claude\skills\$ServerName") # config is manual; skill still useful
179
- )
180
- foreach ($dir in $skillDirs) { Sync-Skill 'skill' $dir }
181
-
182
- Write-Step 'done. Manual targets (see agents/README.md): Claude Code (project .mcp.json), DSH (cordis.patch.yml).'
183
- Write-Step 'Restart agent sessions to pick up the new MCP server.'
48
+ throw "agent-comm-hub is not installed and $local does not exist. Install it (`npm i -g agent-comm-hub`) or run `pnpm run build` in a checkout, then use `agent-comm-hub setup`."