cross-agent-teams-mcp 0.5.0 → 0.5.2
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.zh-CN.md +29 -59
- package/dist/channel-cli.js +39 -23
- package/dist/channel-cli.js.map +1 -1
- package/dist/cli.js +78 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/daemon/server.ts +11 -1
- package/src/mcp/register-agent.ts +32 -2
- package/src/mcp/tools.ts +4 -2
- package/src/mcp/transport.ts +60 -5
package/README.zh-CN.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[English README](./README.md)
|
|
4
4
|
|
|
5
|
-
一个本地 MCP daemon, 让同一台机器上的多个 AI 编码 agent (Claude Code, Codex, opencode) 互相通信. agent 注册到 daemon, 互发 1-to-1 消息, 在 team 或 role 内广播,
|
|
5
|
+
一个本地 MCP daemon, 让同一台机器上的多个 AI 编码 agent (Claude Code, Codex, opencode) 互相通信. agent 注册到 daemon, 互发 1-to-1 消息, 在 team 或 role 内广播, 互相唤醒 — 全部通过一个本地 daemon 完成, 不依赖任何外部服务.
|
|
6
6
|
|
|
7
7
|
## npm 包内容
|
|
8
8
|
|
|
9
9
|
`cross-agent-teams-mcp` 在同一个包里发两个 bin:
|
|
10
10
|
|
|
11
|
-
- **`cross-agent-teams-mcp daemon`** — 长驻 HTTP daemon. 把 agent
|
|
11
|
+
- **`cross-agent-teams-mcp daemon`** — 长驻 HTTP daemon. 把 agent 注册表和邮箱存在本地 SQLite 文件里, MCP endpoint 在 `http://127.0.0.1:9100/mcp`.
|
|
12
12
|
- **`cross-agent-teams-channel`** — stdio MCP shim, 让 Claude Code 通过 `notifications/channel_wake` 接收唤醒通知 (Claude Code 的 experimental channel capability). Claude Code 需要它接收 wake; Codex 用自己的 app-server 通道, opencode 走 tmux-pane 文本注入, 都不需要 channel proxy.
|
|
13
13
|
|
|
14
14
|
## 1. 启动 daemon
|
|
@@ -89,82 +89,52 @@ codex --remote ws://127.0.0.1:8799 # 另一个终端 (TUI)
|
|
|
89
89
|
|
|
90
90
|
详细配置 (auth header, tmux fallback, 底层 `register_agent` 用法): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
|
|
91
91
|
|
|
92
|
-
### opencode
|
|
92
|
+
### 其它编码 agent (opencode, cursor, ...)
|
|
93
93
|
|
|
94
|
-
opencode 直接通过 Streamable HTTP 连 daemon
|
|
94
|
+
非 Claude Code 也非 Codex 的工具 — opencode, cursor, 编辑器扩展, 自己的 harness — 直接通过 Streamable HTTP 连 daemon, 注册时用 `agent_type="custom"` (agent 自己会判断). 这些 agent 没有专用的唤醒通道; 跨 agent poke 通过把文本注入到 agent 所在的 tmux pane 实现, 所以把 agent 跑在 tmux 窗口里, 注册时 daemon 会自动解析 `pid → tty → pane`.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
各工具的具体配置片段在 [docs/configs/opencode.md](docs/configs/opencode.md) (其它在 `docs/configs/`).
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
## 3. 从 agent 里使用
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
agent 连上 daemon 后, 你不需要去记工具名字. 直接用平时跟 agent 对话的语言告诉它你想干嘛, agent 会自己挑工具 — 下面列的是 *你说的话*, 不是底层 API.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
> 注意: 这些都要在 agent 会话内说. 不要用 `curl` 或其它外部 HTTP client 去注册或发消息 — 那会开一个不同的 MCP session, 消息送不到你这里.
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
2. `printenv CLAUDECODE` 非空 或 `printenv CLAUDE_CODE_ENTRYPOINT` 非空 → `agent_type="claude-code"`; 把 `$PPID` 传为 `ui_pid`.
|
|
106
|
-
3. 以上都不满足 → `agent_type="custom"`, `agent_type_name="<你的 harness 名字, e.g. cursor, opencode, ...>"`. 能从 runtime 环境检测出 harness 名就传 — 例如 `printenv CURSOR_TRACE_ID` 非空就是 cursor — 但**不要**用"PATH 上有某个二进制"这类系统级信号去猜: 那只反映用户装了什么, 不代表 LLM 跑在那个 runtime 里, 会选错 agent_type. 没把握时 `agent_type_name="unknown"` 比猜错好.
|
|
104
|
+
### 注册当前会话
|
|
107
105
|
|
|
108
|
-
|
|
106
|
+
agent 第一次连上 xats 时不会自动注册, 要等你开口. 直接说:
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
register_agent({
|
|
112
|
-
agent_type: "codex",
|
|
113
|
-
name: "<agent-name>",
|
|
114
|
-
thread_id: "<$CODEX_THREAD_ID 的值>",
|
|
115
|
-
project_dir: "<项目的绝对路径>"
|
|
116
|
-
})
|
|
117
|
-
```
|
|
108
|
+
> Register me to xats as alice.
|
|
118
109
|
|
|
119
|
-
|
|
110
|
+
或者指定 team:
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
register_agent({
|
|
123
|
-
agent_type: "claude-code",
|
|
124
|
-
name: "<agent-name>",
|
|
125
|
-
ui_pid: <Claude Code CLI 的 pid; 在 Bash 工具里就是 $PPID>,
|
|
126
|
-
project_dir: "<项目的绝对路径>"
|
|
127
|
-
})
|
|
128
|
-
```
|
|
112
|
+
> Register me to xats as alice on team backend.
|
|
129
113
|
|
|
130
|
-
|
|
114
|
+
不传 team 的话, agent 会用当前工作目录的 basename 作为默认 team — 一般情况下你不用操心.
|
|
131
115
|
|
|
132
|
-
|
|
133
|
-
register_agent({
|
|
134
|
-
agent_type: "custom",
|
|
135
|
-
agent_type_name: "<你的 harness 名字>", // agent_type="custom" 时必填
|
|
136
|
-
name: "<agent-name>",
|
|
137
|
-
project_dir: "<项目的绝对路径>",
|
|
138
|
-
ui_pid: <runtime pid> // tmux poke 通道强烈建议传
|
|
139
|
-
})
|
|
140
|
-
```
|
|
116
|
+
### 跟其它 agent 对话
|
|
141
117
|
|
|
142
|
-
|
|
118
|
+
按名字, 按 team, 按 role 都行:
|
|
143
119
|
|
|
144
|
-
|
|
120
|
+
> Send a message to bob: how is the migration going?
|
|
121
|
+
>
|
|
122
|
+
> Tell my team I'm starting the deploy.
|
|
123
|
+
>
|
|
124
|
+
> Send the frontend role a heads-up that the API will change.
|
|
125
|
+
>
|
|
126
|
+
> What's in my inbox?
|
|
145
127
|
|
|
146
|
-
|
|
128
|
+
agent 会自动挑对应工具 (`send_message`, `broadcast`, `broadcast_to_role`, `get_inbox`). 发消息的同时会自动唤醒收件人, 不用单独再 poke.
|
|
147
129
|
|
|
148
|
-
|
|
149
|
-
send_message({ to_agent_name: "<对方名字>", subject: "...", body: "..." })
|
|
150
|
-
broadcast({ subject: "...", body: "..." }) // 同 team 广播
|
|
151
|
-
broadcast_to_role({ role: "<role>", subject, body }) // 同 team 同 role
|
|
152
|
-
get_inbox() // 看自己的收件箱
|
|
153
|
-
```
|
|
130
|
+
### 看看还有谁在线
|
|
154
131
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```text
|
|
160
|
-
task_add({ title, description? })
|
|
161
|
-
task_list({ status?: "open" | "claimed" | "done" })
|
|
162
|
-
task_claim({ task_id })
|
|
163
|
-
task_complete({ task_id, result? })
|
|
164
|
-
```
|
|
132
|
+
> Who else is registered on xats?
|
|
133
|
+
>
|
|
134
|
+
> List agents on team backend.
|
|
165
135
|
|
|
166
136
|
## 更多
|
|
167
137
|
|
|
168
138
|
- 完整工具列表和参数: 启动 daemon 后调 MCP endpoint 的 `tools/list`.
|
|
169
|
-
-
|
|
139
|
+
- 各 agent 详细配置: `docs/configs/`.
|
|
170
140
|
- 源码: [github.com/jtianling/cross-agent-teams-mcp](https://github.com/jtianling/cross-agent-teams-mcp).
|
package/dist/channel-cli.js
CHANGED
|
@@ -140,38 +140,54 @@ async function runRegistrationSequence(config) {
|
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
async function waitForDisconnect(seq, opts = {}) {
|
|
144
|
+
const interval = opts.healthCheckIntervalMs ?? 3e4;
|
|
145
|
+
const shouldStop = opts.shouldStop ?? (() => false);
|
|
146
|
+
let disconnected = false;
|
|
147
|
+
let wakeup = null;
|
|
148
|
+
const closeHandler = () => {
|
|
149
|
+
disconnected = true;
|
|
150
|
+
wakeup?.();
|
|
151
|
+
};
|
|
152
|
+
const prevOnClose = seq.transport.onclose;
|
|
153
|
+
seq.transport.onclose = () => {
|
|
154
|
+
prevOnClose?.();
|
|
155
|
+
closeHandler();
|
|
156
|
+
};
|
|
157
|
+
while (!disconnected && !shouldStop()) {
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
wakeup = null;
|
|
161
|
+
resolve();
|
|
162
|
+
}, interval);
|
|
163
|
+
wakeup = () => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
wakeup = null;
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
if (disconnected || shouldStop()) break;
|
|
170
|
+
try {
|
|
171
|
+
await seq.client.callTool({ name: "echo", arguments: { msg: "hb" } });
|
|
172
|
+
} catch {
|
|
150
173
|
disconnected = true;
|
|
151
|
-
|
|
152
|
-
const prevOnClose = seq.transport.onclose;
|
|
153
|
-
seq.transport.onclose = () => {
|
|
154
|
-
prevOnClose?.();
|
|
155
|
-
closeHandler();
|
|
156
|
-
};
|
|
157
|
-
while (!disconnected && !stopped) {
|
|
158
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
159
|
-
if (disconnected || stopped) break;
|
|
160
|
-
try {
|
|
161
|
-
await seq.client.callTool({ name: "echo", arguments: { msg: "hb" } });
|
|
162
|
-
} catch {
|
|
163
|
-
disconnected = true;
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
174
|
+
break;
|
|
166
175
|
}
|
|
167
176
|
}
|
|
177
|
+
}
|
|
178
|
+
function runReconnectingProxy(config) {
|
|
179
|
+
let stopped = false;
|
|
180
|
+
let currentSeq = null;
|
|
168
181
|
async function loop() {
|
|
169
182
|
while (!stopped) {
|
|
170
183
|
try {
|
|
171
184
|
const seq = await runRegistrationSequence(config);
|
|
172
185
|
currentSeq = seq;
|
|
173
186
|
if (config.onSequenceComplete) config.onSequenceComplete([...seq.order]);
|
|
174
|
-
await waitForDisconnect(seq
|
|
187
|
+
await waitForDisconnect(seq, {
|
|
188
|
+
healthCheckIntervalMs: config.healthCheckIntervalMs,
|
|
189
|
+
shouldStop: () => stopped
|
|
190
|
+
});
|
|
175
191
|
if (config.onDisconnect) config.onDisconnect();
|
|
176
192
|
try {
|
|
177
193
|
await seq.close();
|
package/dist/channel-cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../plugins/cross-agent-teams-channel/src/cli.ts","../plugins/cross-agent-teams-channel/src/proxy.ts","../plugins/cross-agent-teams-channel/src/daemon-client.ts","../plugins/cross-agent-teams-channel/src/find-claude-pid.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { randomUUID } from 'node:crypto'\nimport { realpathSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createProxyServer, relayChannelWake } from './proxy.js'\nimport { runReconnectingProxy } from './daemon-client.js'\n\ninterface CliArgs {\n daemonUrl: string\n}\n\nexport class CliArgError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliArgError'\n }\n}\n\nexport function buildStartupHint(csid: string): { content: string; meta: { source: string; kind: string } } {\n const content = [\n `cross-agent-teams-mcp: your channel_session_id is ${csid}.`,\n `Do NOT register automatically. First ask the user (in English) to register this session so it can talk to other agents — use exactly this wording: 'To join cross-agent-teams (xats) and message other agents, reply with: Register to xats — name: your-agent-name, team: your-team-name (optional; defaults to the current working directory basename).'`,\n `Once the user provides a name (and optionally a team), call register_agent({agent_type: \"claude-code\", name: \"<name from user>\", team: \"<team from user, omit if not provided>\", ui_pid: $PPID, project_dir: \"<current working directory>\"}) — do NOT pass channel_session_id here; the daemon auto-binds via ui_pid.`,\n `bind_channel({channel_session_id: \"${csid}\"}) is the low-level rebind tool for an already-registered Claude host that needs to switch to a fresh csid; it is NOT the primary registration path.`,\n `Do not use curl or another external HTTP client for Claude registration here — that would create a different MCP session, and follow-up tools in Claude Code could still see unknown_agent.`\n ].join(' ')\n return {\n content,\n meta: { source: 'cross_agent_teams_mcp', kind: 'startup_bind_hint' }\n }\n}\n\nexport function parseCliArgs(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): CliArgs {\n let daemonUrl: string | undefined\n\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]\n const next = argv[i + 1]\n switch (flag) {\n case '--daemon-url':\n daemonUrl = next; i++; break\n default:\n // Ignore unknown flags for forward-compat (including legacy\n // --agent-team / --agent-name, which are no longer honored).\n break\n }\n }\n\n if (!daemonUrl || daemonUrl.length === 0) {\n daemonUrl = env.CROSS_AGENT_TEAMS_MCP_DAEMON_URL\n }\n\n if (!daemonUrl || daemonUrl.length === 0) {\n throw new CliArgError(\n 'missing --daemon-url (or CROSS_AGENT_TEAMS_MCP_DAEMON_URL env var)'\n )\n }\n return { daemonUrl }\n}\n\nexport async function main(\n argv: readonly string[] = process.argv.slice(2),\n env: NodeJS.ProcessEnv = process.env\n): Promise<void> {\n let args: CliArgs\n try {\n args = parseCliArgs(argv, env)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n process.stderr.write(`cross-agent-teams-proxy: ${msg}\\n`)\n process.exit(2)\n }\n\n // Fresh csid per startup — no persistence. Multi-instance safe.\n const csid = randomUUID()\n\n const hostServer = createProxyServer()\n const stdioTransport = new StdioServerTransport()\n\n let registrationEverSucceeded = false\n const controller = runReconnectingProxy({\n daemonUrl: args.daemonUrl,\n channel_session_id: csid,\n notificationHandler: (params) => {\n relayChannelWake(hostServer, params as { content: string; meta: Record<string, string> })\n },\n onSequenceComplete: () => {\n registrationEverSucceeded = true\n // Announce csid to Claude via host-facing channel notification so Claude\n // can call bind_channel({channel_session_id}) to bind its own agent row.\n const hint = buildStartupHint(csid)\n relayChannelWake(hostServer, hint)\n }\n })\n\n let stopped = false\n const shutdown = async (): Promise<void> => {\n if (stopped) return\n stopped = true\n try { await controller.stop() } catch { /* best-effort */ }\n try { await hostServer.close() } catch { /* best-effort */ }\n if (!registrationEverSucceeded) {\n process.stderr.write(`cross-agent-teams-proxy: daemon unreachable at ${args.daemonUrl}\\n`)\n process.exit(1)\n }\n process.exit(0)\n }\n\n stdioTransport.onclose = () => { void shutdown() }\n\n await hostServer.connect(stdioTransport)\n\n process.on('SIGTERM', () => { void shutdown() })\n process.on('SIGINT', () => { void shutdown() })\n}\n\n// Entry-point check. The naive `import.meta.url === \\`file://${process.argv[1]}\\``\n// breaks when launched via an npm `.bin` symlink (npx, `npm install -g`):\n// process.argv[1] is the symlink path, while import.meta.url is already\n// resolved. Compare realpath-resolved file paths instead.\nfunction isEntry(): boolean {\n try {\n const metaPath = fileURLToPath(import.meta.url)\n const argvPath = realpathSync(process.argv[1])\n return metaPath === argvPath\n } catch {\n return false\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises\nif (isEntry()) {\n void main()\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\n\nexport function createProxyServer(): McpServer {\n return new McpServer(\n { name: 'cross-agent-teams-channel', version: '0.1.0' },\n { capabilities: { experimental: { 'claude/channel': {} } } }\n )\n}\n\nexport interface ChannelWakeParams {\n content: string\n meta: Record<string, string>\n}\n\nexport function relayChannelWake(server: McpServer, params: ChannelWakeParams): void {\n try {\n const notif = {\n method: 'notifications/claude/channel',\n params: params as unknown as Record<string, unknown>\n }\n const p = (server.server.notification as (n: typeof notif) => Promise<void>)(notif)\n if (p && typeof p.catch === 'function') {\n p.catch(() => { /* host closed — drop silently */ })\n }\n } catch {\n // host transport closed or not yet connected — drop silently\n }\n}\n","import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { findClaudeUiPid } from './find-claude-pid.js'\n\nexport interface RegistrationConfig {\n daemonUrl: string\n channel_session_id: string\n backoffInitialMs?: number\n backoffMaxMs?: number\n notificationHandler?: (payload: unknown) => void\n}\n\nexport interface ReconnectingProxyConfig extends RegistrationConfig {\n onSequenceComplete?: (order: string[]) => void\n onDisconnect?: () => void\n healthCheckIntervalMs?: number\n}\n\nexport interface ReconnectingProxyController {\n stop(): Promise<void>\n}\n\nexport interface RegistrationSequenceResult {\n order: string[]\n lastSubscribeResult: unknown\n client: Client\n transport: StreamableHTTPClientTransport\n close: () => Promise<void>\n}\n\ntype ToolResult = Record<string, unknown>\n\nasync function parseToolResult(resp: unknown): Promise<ToolResult> {\n const r = resp as { content?: Array<{ text?: string }> }\n const text = r.content?.[0]?.text\n if (typeof text !== 'string') return {}\n try { return JSON.parse(text) as ToolResult } catch { return {} }\n}\n\nexport async function runRegistrationSequence(\n config: RegistrationConfig\n): Promise<RegistrationSequenceResult> {\n const order: string[] = []\n const transport = new StreamableHTTPClientTransport(new URL(config.daemonUrl))\n const client = new Client({ name: 'cross-agent-teams-proxy', version: '0.1.0' })\n\n if (config.notificationHandler) {\n client.fallbackNotificationHandler = async (n) => {\n if (n.method === 'notifications/channel_wake') {\n config.notificationHandler!(n.params)\n }\n }\n }\n\n await client.connect(transport)\n\n // 1. register_agent as proxy — identity keyed on pid, stable across reconnects\n // so the (team, name) ON CONFLICT upsert reuses the same row instead of spamming new rows\n const registerResp = await client.callTool({\n name: 'register_agent',\n arguments: {\n agent_type: 'custom',\n agent_type_name: 'cross-agent-teams-channel',\n model: 'proxy',\n role: '__channel_proxy__',\n name: `channel-proxy-${process.pid}`,\n team: 'default',\n claude_ui_pid: findClaudeUiPid(),\n delivery: {\n kind: 'claude-channel',\n channel_session_id: config.channel_session_id,\n },\n }\n })\n order.push('register_agent')\n const regResult = await parseToolResult(registerResp)\n if (!('agent_id' in regResult)) {\n throw new Error(`register_agent failed: ${JSON.stringify(regResult)}`)\n }\n\n // 2. subscribe_channel_wake — proxy's csid is fresh per startup\n const subResp = await client.callTool({\n name: 'subscribe_channel_wake',\n arguments: { channel_session_id: config.channel_session_id }\n })\n order.push('subscribe_channel_wake')\n const subResult = await parseToolResult(subResp)\n if (!('ok' in subResult) || subResult.ok !== true) {\n throw new Error(`subscribe_channel_wake failed: ${JSON.stringify(subResult)}`)\n }\n\n return {\n order,\n lastSubscribeResult: subResult,\n client,\n transport,\n close: async () => {\n try { await client.close() } catch { /* best-effort */ }\n try { await transport.close() } catch { /* best-effort */ }\n }\n }\n}\n\nexport function runReconnectingProxy(config: ReconnectingProxyConfig): ReconnectingProxyController {\n let stopped = false\n let currentSeq: RegistrationSequenceResult | null = null\n\n async function waitForDisconnect(seq: RegistrationSequenceResult): Promise<void> {\n const interval = config.healthCheckIntervalMs ?? 200\n let disconnected = false\n const closeHandler = () => { disconnected = true }\n const prevOnClose = seq.transport.onclose\n seq.transport.onclose = () => { prevOnClose?.(); closeHandler() }\n while (!disconnected && !stopped) {\n await new Promise(r => setTimeout(r, interval))\n if (disconnected || stopped) break\n try {\n await seq.client.callTool({ name: 'echo', arguments: { msg: 'hb' } })\n } catch {\n disconnected = true\n break\n }\n }\n }\n\n async function loop(): Promise<void> {\n while (!stopped) {\n try {\n const seq = await runRegistrationSequence(config)\n currentSeq = seq\n if (config.onSequenceComplete) config.onSequenceComplete([...seq.order])\n\n await waitForDisconnect(seq)\n if (config.onDisconnect) config.onDisconnect()\n try { await seq.close() } catch { /* best-effort */ }\n currentSeq = null\n } catch {\n // register/subscribe failed — wait and retry.\n }\n if (stopped) break\n const wait = config.backoffInitialMs ?? 500\n await new Promise(r => setTimeout(r, wait))\n }\n }\n\n void loop()\n\n return {\n stop: async () => {\n stopped = true\n if (currentSeq) {\n try { await currentSeq.close() } catch { /* best-effort */ }\n }\n }\n }\n}\n","import { execFileSync } from 'node:child_process'\n\nconst MAX_HOPS = 8\n\ninterface PsRow {\n ppid: number\n cmd: string\n}\n\nexport function readPsRow(pid: number): PsRow | null {\n try {\n const out = execFileSync('ps', ['-o', 'ppid=,args=', '-p', String(pid)], {\n encoding: 'utf8',\n timeout: 1000,\n stdio: ['ignore', 'pipe', 'ignore']\n })\n const trimmed = out.trim()\n if (!trimmed) return null\n const m = /^\\s*(\\d+)\\s+(.*)$/.exec(trimmed)\n if (!m) return null\n return { ppid: parseInt(m[1], 10), cmd: m[2] }\n } catch {\n return null\n }\n}\n\nexport function isClaudeCmd(cmd: string): boolean {\n const first = cmd.trim().split(/\\s+/)[0]\n if (!first) return false\n const base = first.replace(/^.*\\//, '')\n return base === 'claude'\n}\n\nexport function findClaudeUiPid(\n startPpid: number = process.ppid,\n reader: (pid: number) => PsRow | null = readPsRow\n): number {\n let pid = startPpid\n for (let i = 0; i < MAX_HOPS; i++) {\n const row = reader(pid)\n if (!row) break\n if (isClaudeCmd(row.cmd)) return pid\n if (row.ppid <= 1 || row.ppid === pid) break\n pid = row.ppid\n }\n return startPpid\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../plugins/cross-agent-teams-channel/src/cli.ts","../plugins/cross-agent-teams-channel/src/proxy.ts","../plugins/cross-agent-teams-channel/src/daemon-client.ts","../plugins/cross-agent-teams-channel/src/find-claude-pid.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { randomUUID } from 'node:crypto'\nimport { realpathSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createProxyServer, relayChannelWake } from './proxy.js'\nimport { runReconnectingProxy } from './daemon-client.js'\n\ninterface CliArgs {\n daemonUrl: string\n}\n\nexport class CliArgError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliArgError'\n }\n}\n\nexport function buildStartupHint(csid: string): { content: string; meta: { source: string; kind: string } } {\n const content = [\n `cross-agent-teams-mcp: your channel_session_id is ${csid}.`,\n `Do NOT register automatically. First ask the user (in English) to register this session so it can talk to other agents — use exactly this wording: 'To join cross-agent-teams (xats) and message other agents, reply with: Register to xats — name: your-agent-name, team: your-team-name (optional; defaults to the current working directory basename).'`,\n `Once the user provides a name (and optionally a team), call register_agent({agent_type: \"claude-code\", name: \"<name from user>\", team: \"<team from user, omit if not provided>\", ui_pid: $PPID, project_dir: \"<current working directory>\"}) — do NOT pass channel_session_id here; the daemon auto-binds via ui_pid.`,\n `bind_channel({channel_session_id: \"${csid}\"}) is the low-level rebind tool for an already-registered Claude host that needs to switch to a fresh csid; it is NOT the primary registration path.`,\n `Do not use curl or another external HTTP client for Claude registration here — that would create a different MCP session, and follow-up tools in Claude Code could still see unknown_agent.`\n ].join(' ')\n return {\n content,\n meta: { source: 'cross_agent_teams_mcp', kind: 'startup_bind_hint' }\n }\n}\n\nexport function parseCliArgs(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): CliArgs {\n let daemonUrl: string | undefined\n\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]\n const next = argv[i + 1]\n switch (flag) {\n case '--daemon-url':\n daemonUrl = next; i++; break\n default:\n // Ignore unknown flags for forward-compat (including legacy\n // --agent-team / --agent-name, which are no longer honored).\n break\n }\n }\n\n if (!daemonUrl || daemonUrl.length === 0) {\n daemonUrl = env.CROSS_AGENT_TEAMS_MCP_DAEMON_URL\n }\n\n if (!daemonUrl || daemonUrl.length === 0) {\n throw new CliArgError(\n 'missing --daemon-url (or CROSS_AGENT_TEAMS_MCP_DAEMON_URL env var)'\n )\n }\n return { daemonUrl }\n}\n\nexport async function main(\n argv: readonly string[] = process.argv.slice(2),\n env: NodeJS.ProcessEnv = process.env\n): Promise<void> {\n let args: CliArgs\n try {\n args = parseCliArgs(argv, env)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n process.stderr.write(`cross-agent-teams-proxy: ${msg}\\n`)\n process.exit(2)\n }\n\n // Fresh csid per startup — no persistence. Multi-instance safe.\n const csid = randomUUID()\n\n const hostServer = createProxyServer()\n const stdioTransport = new StdioServerTransport()\n\n let registrationEverSucceeded = false\n const controller = runReconnectingProxy({\n daemonUrl: args.daemonUrl,\n channel_session_id: csid,\n notificationHandler: (params) => {\n relayChannelWake(hostServer, params as { content: string; meta: Record<string, string> })\n },\n onSequenceComplete: () => {\n registrationEverSucceeded = true\n // Announce csid to Claude via host-facing channel notification so Claude\n // can call bind_channel({channel_session_id}) to bind its own agent row.\n const hint = buildStartupHint(csid)\n relayChannelWake(hostServer, hint)\n }\n })\n\n let stopped = false\n const shutdown = async (): Promise<void> => {\n if (stopped) return\n stopped = true\n try { await controller.stop() } catch { /* best-effort */ }\n try { await hostServer.close() } catch { /* best-effort */ }\n if (!registrationEverSucceeded) {\n process.stderr.write(`cross-agent-teams-proxy: daemon unreachable at ${args.daemonUrl}\\n`)\n process.exit(1)\n }\n process.exit(0)\n }\n\n stdioTransport.onclose = () => { void shutdown() }\n\n await hostServer.connect(stdioTransport)\n\n process.on('SIGTERM', () => { void shutdown() })\n process.on('SIGINT', () => { void shutdown() })\n}\n\n// Entry-point check. The naive `import.meta.url === \\`file://${process.argv[1]}\\``\n// breaks when launched via an npm `.bin` symlink (npx, `npm install -g`):\n// process.argv[1] is the symlink path, while import.meta.url is already\n// resolved. Compare realpath-resolved file paths instead.\nfunction isEntry(): boolean {\n try {\n const metaPath = fileURLToPath(import.meta.url)\n const argvPath = realpathSync(process.argv[1])\n return metaPath === argvPath\n } catch {\n return false\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises\nif (isEntry()) {\n void main()\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\n\nexport function createProxyServer(): McpServer {\n return new McpServer(\n { name: 'cross-agent-teams-channel', version: '0.1.0' },\n { capabilities: { experimental: { 'claude/channel': {} } } }\n )\n}\n\nexport interface ChannelWakeParams {\n content: string\n meta: Record<string, string>\n}\n\nexport function relayChannelWake(server: McpServer, params: ChannelWakeParams): void {\n try {\n const notif = {\n method: 'notifications/claude/channel',\n params: params as unknown as Record<string, unknown>\n }\n const p = (server.server.notification as (n: typeof notif) => Promise<void>)(notif)\n if (p && typeof p.catch === 'function') {\n p.catch(() => { /* host closed — drop silently */ })\n }\n } catch {\n // host transport closed or not yet connected — drop silently\n }\n}\n","import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { findClaudeUiPid } from './find-claude-pid.js'\n\nexport interface RegistrationConfig {\n daemonUrl: string\n channel_session_id: string\n backoffInitialMs?: number\n backoffMaxMs?: number\n notificationHandler?: (payload: unknown) => void\n}\n\nexport interface ReconnectingProxyConfig extends RegistrationConfig {\n onSequenceComplete?: (order: string[]) => void\n onDisconnect?: () => void\n healthCheckIntervalMs?: number\n}\n\nexport interface ReconnectingProxyController {\n stop(): Promise<void>\n}\n\nexport interface RegistrationSequenceResult {\n order: string[]\n lastSubscribeResult: unknown\n client: Client\n transport: StreamableHTTPClientTransport\n close: () => Promise<void>\n}\n\ntype ToolResult = Record<string, unknown>\n\nasync function parseToolResult(resp: unknown): Promise<ToolResult> {\n const r = resp as { content?: Array<{ text?: string }> }\n const text = r.content?.[0]?.text\n if (typeof text !== 'string') return {}\n try { return JSON.parse(text) as ToolResult } catch { return {} }\n}\n\nexport async function runRegistrationSequence(\n config: RegistrationConfig\n): Promise<RegistrationSequenceResult> {\n const order: string[] = []\n const transport = new StreamableHTTPClientTransport(new URL(config.daemonUrl))\n const client = new Client({ name: 'cross-agent-teams-proxy', version: '0.1.0' })\n\n if (config.notificationHandler) {\n client.fallbackNotificationHandler = async (n) => {\n if (n.method === 'notifications/channel_wake') {\n config.notificationHandler!(n.params)\n }\n }\n }\n\n await client.connect(transport)\n\n // 1. register_agent as proxy — identity keyed on pid, stable across reconnects\n // so the (team, name) ON CONFLICT upsert reuses the same row instead of spamming new rows\n const registerResp = await client.callTool({\n name: 'register_agent',\n arguments: {\n agent_type: 'custom',\n agent_type_name: 'cross-agent-teams-channel',\n model: 'proxy',\n role: '__channel_proxy__',\n name: `channel-proxy-${process.pid}`,\n team: 'default',\n claude_ui_pid: findClaudeUiPid(),\n delivery: {\n kind: 'claude-channel',\n channel_session_id: config.channel_session_id,\n },\n }\n })\n order.push('register_agent')\n const regResult = await parseToolResult(registerResp)\n if (!('agent_id' in regResult)) {\n throw new Error(`register_agent failed: ${JSON.stringify(regResult)}`)\n }\n\n // 2. subscribe_channel_wake — proxy's csid is fresh per startup\n const subResp = await client.callTool({\n name: 'subscribe_channel_wake',\n arguments: { channel_session_id: config.channel_session_id }\n })\n order.push('subscribe_channel_wake')\n const subResult = await parseToolResult(subResp)\n if (!('ok' in subResult) || subResult.ok !== true) {\n throw new Error(`subscribe_channel_wake failed: ${JSON.stringify(subResult)}`)\n }\n\n return {\n order,\n lastSubscribeResult: subResult,\n client,\n transport,\n close: async () => {\n try { await client.close() } catch { /* best-effort */ }\n try { await transport.close() } catch { /* best-effort */ }\n }\n }\n}\n\nexport interface WaitForDisconnectInput {\n client: Pick<Client, 'callTool'>\n transport: { onclose?: (() => void) | null | undefined }\n}\n\nexport interface WaitForDisconnectOptions {\n healthCheckIntervalMs?: number\n shouldStop?: () => boolean\n}\n\nexport async function waitForDisconnect(\n seq: WaitForDisconnectInput,\n opts: WaitForDisconnectOptions = {}\n): Promise<void> {\n const interval = opts.healthCheckIntervalMs ?? 30_000\n const shouldStop = opts.shouldStop ?? (() => false)\n let disconnected = false\n let wakeup: (() => void) | null = null\n const closeHandler = (): void => {\n disconnected = true\n wakeup?.()\n }\n const prevOnClose = seq.transport.onclose\n seq.transport.onclose = (): void => { prevOnClose?.(); closeHandler() }\n while (!disconnected && !shouldStop()) {\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => { wakeup = null; resolve() }, interval)\n wakeup = (): void => { clearTimeout(timer); wakeup = null; resolve() }\n })\n if (disconnected || shouldStop()) break\n try {\n await seq.client.callTool({ name: 'echo', arguments: { msg: 'hb' } })\n } catch {\n disconnected = true\n break\n }\n }\n}\n\nexport function runReconnectingProxy(config: ReconnectingProxyConfig): ReconnectingProxyController {\n let stopped = false\n let currentSeq: RegistrationSequenceResult | null = null\n\n async function loop(): Promise<void> {\n while (!stopped) {\n try {\n const seq = await runRegistrationSequence(config)\n currentSeq = seq\n if (config.onSequenceComplete) config.onSequenceComplete([...seq.order])\n\n await waitForDisconnect(seq, {\n healthCheckIntervalMs: config.healthCheckIntervalMs,\n shouldStop: () => stopped,\n })\n if (config.onDisconnect) config.onDisconnect()\n try { await seq.close() } catch { /* best-effort */ }\n currentSeq = null\n } catch {\n // register/subscribe failed — wait and retry.\n }\n if (stopped) break\n const wait = config.backoffInitialMs ?? 500\n await new Promise(r => setTimeout(r, wait))\n }\n }\n\n void loop()\n\n return {\n stop: async () => {\n stopped = true\n if (currentSeq) {\n try { await currentSeq.close() } catch { /* best-effort */ }\n }\n }\n }\n}\n","import { execFileSync } from 'node:child_process'\n\nconst MAX_HOPS = 8\n\ninterface PsRow {\n ppid: number\n cmd: string\n}\n\nexport function readPsRow(pid: number): PsRow | null {\n try {\n const out = execFileSync('ps', ['-o', 'ppid=,args=', '-p', String(pid)], {\n encoding: 'utf8',\n timeout: 1000,\n stdio: ['ignore', 'pipe', 'ignore']\n })\n const trimmed = out.trim()\n if (!trimmed) return null\n const m = /^\\s*(\\d+)\\s+(.*)$/.exec(trimmed)\n if (!m) return null\n return { ppid: parseInt(m[1], 10), cmd: m[2] }\n } catch {\n return null\n }\n}\n\nexport function isClaudeCmd(cmd: string): boolean {\n const first = cmd.trim().split(/\\s+/)[0]\n if (!first) return false\n const base = first.replace(/^.*\\//, '')\n return base === 'claude'\n}\n\nexport function findClaudeUiPid(\n startPpid: number = process.ppid,\n reader: (pid: number) => PsRow | null = readPsRow\n): number {\n let pid = startPpid\n for (let i = 0; i < MAX_HOPS; i++) {\n const row = reader(pid)\n if (!row) break\n if (isClaudeCmd(row.cmd)) return pid\n if (row.ppid <= 1 || row.ppid === pid) break\n pid = row.ppid\n }\n return startPpid\n}\n"],"mappings":";;;AACA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,4BAA4B;;;ACJrC,SAAS,iBAAiB;AAEnB,SAAS,oBAA+B;AAC7C,SAAO,IAAI;AAAA,IACT,EAAE,MAAM,6BAA6B,SAAS,QAAQ;AAAA,IACtD,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,CAAC,EAAE,EAAE,EAAE;AAAA,EAC7D;AACF;AAOO,SAAS,iBAAiB,QAAmB,QAAiC;AACnF,MAAI;AACF,UAAM,QAAQ;AAAA,MACZ,QAAQ;AAAA,MACR;AAAA,IACF;AACA,UAAM,IAAK,OAAO,OAAO,aAAoD,KAAK;AAClF,QAAI,KAAK,OAAO,EAAE,UAAU,YAAY;AACtC,QAAE,MAAM,MAAM;AAAA,MAAoC,CAAC;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AACF;;;AC3BA,SAAS,cAAc;AACvB,SAAS,qCAAqC;;;ACD9C,SAAS,oBAAoB;AAE7B,IAAM,WAAW;AAOV,SAAS,UAAU,KAA2B;AACnD,MAAI;AACF,UAAM,MAAM,aAAa,MAAM,CAAC,MAAM,eAAe,MAAM,OAAO,GAAG,CAAC,GAAG;AAAA,MACvE,UAAU;AAAA,MACV,SAAS;AAAA,MACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC;AACD,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,IAAI,oBAAoB,KAAK,OAAO;AAC1C,QAAI,CAAC,EAAG,QAAO;AACf,WAAO,EAAE,MAAM,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,EAAE;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,YAAY,KAAsB;AAChD,QAAM,QAAQ,IAAI,KAAK,EAAE,MAAM,KAAK,EAAE,CAAC;AACvC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MAAM,QAAQ,SAAS,EAAE;AACtC,SAAO,SAAS;AAClB;AAEO,SAAS,gBACd,YAAoB,QAAQ,MAC5B,SAAwC,WAChC;AACR,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,UAAU,KAAK;AACjC,UAAM,MAAM,OAAO,GAAG;AACtB,QAAI,CAAC,IAAK;AACV,QAAI,YAAY,IAAI,GAAG,EAAG,QAAO;AACjC,QAAI,IAAI,QAAQ,KAAK,IAAI,SAAS,IAAK;AACvC,UAAM,IAAI;AAAA,EACZ;AACA,SAAO;AACT;;;ADdA,eAAe,gBAAgB,MAAoC;AACjE,QAAM,IAAI;AACV,QAAM,OAAO,EAAE,UAAU,CAAC,GAAG;AAC7B,MAAI,OAAO,SAAS,SAAU,QAAO,CAAC;AACtC,MAAI;AAAE,WAAO,KAAK,MAAM,IAAI;AAAA,EAAgB,QAAQ;AAAE,WAAO,CAAC;AAAA,EAAE;AAClE;AAEA,eAAsB,wBACpB,QACqC;AACrC,QAAM,QAAkB,CAAC;AACzB,QAAM,YAAY,IAAI,8BAA8B,IAAI,IAAI,OAAO,SAAS,CAAC;AAC7E,QAAM,SAAS,IAAI,OAAO,EAAE,MAAM,2BAA2B,SAAS,QAAQ,CAAC;AAE/E,MAAI,OAAO,qBAAqB;AAC9B,WAAO,8BAA8B,OAAO,MAAM;AAChD,UAAI,EAAE,WAAW,8BAA8B;AAC7C,eAAO,oBAAqB,EAAE,MAAM;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,SAAS;AAI9B,QAAM,eAAe,MAAM,OAAO,SAAS;AAAA,IACzC,MAAM;AAAA,IACN,WAAW;AAAA,MACT,YAAY;AAAA,MACZ,iBAAiB;AAAA,MACjB,OAAO;AAAA,MACP,MAAM;AAAA,MACN,MAAM,iBAAiB,QAAQ,GAAG;AAAA,MAClC,MAAM;AAAA,MACN,eAAe,gBAAgB;AAAA,MAC/B,UAAU;AAAA,QACR,MAAM;AAAA,QACN,oBAAoB,OAAO;AAAA,MAC7B;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,KAAK,gBAAgB;AAC3B,QAAM,YAAY,MAAM,gBAAgB,YAAY;AACpD,MAAI,EAAE,cAAc,YAAY;AAC9B,UAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,SAAS,CAAC,EAAE;AAAA,EACvE;AAGA,QAAM,UAAU,MAAM,OAAO,SAAS;AAAA,IACpC,MAAM;AAAA,IACN,WAAW,EAAE,oBAAoB,OAAO,mBAAmB;AAAA,EAC7D,CAAC;AACD,QAAM,KAAK,wBAAwB;AACnC,QAAM,YAAY,MAAM,gBAAgB,OAAO;AAC/C,MAAI,EAAE,QAAQ,cAAc,UAAU,OAAO,MAAM;AACjD,UAAM,IAAI,MAAM,kCAAkC,KAAK,UAAU,SAAS,CAAC,EAAE;AAAA,EAC/E;AAEA,SAAO;AAAA,IACL;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA;AAAA,IACA,OAAO,YAAY;AACjB,UAAI;AAAE,cAAM,OAAO,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAoB;AACvD,UAAI;AAAE,cAAM,UAAU,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAoB;AAAA,IAC5D;AAAA,EACF;AACF;AAYA,eAAsB,kBACpB,KACA,OAAiC,CAAC,GACnB;AACf,QAAM,WAAW,KAAK,yBAAyB;AAC/C,QAAM,aAAa,KAAK,eAAe,MAAM;AAC7C,MAAI,eAAe;AACnB,MAAI,SAA8B;AAClC,QAAM,eAAe,MAAY;AAC/B,mBAAe;AACf,aAAS;AAAA,EACX;AACA,QAAM,cAAc,IAAI,UAAU;AAClC,MAAI,UAAU,UAAU,MAAY;AAAE,kBAAc;AAAG,iBAAa;AAAA,EAAE;AACtE,SAAO,CAAC,gBAAgB,CAAC,WAAW,GAAG;AACrC,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,YAAM,QAAQ,WAAW,MAAM;AAAE,iBAAS;AAAM,gBAAQ;AAAA,MAAE,GAAG,QAAQ;AACrE,eAAS,MAAY;AAAE,qBAAa,KAAK;AAAG,iBAAS;AAAM,gBAAQ;AAAA,MAAE;AAAA,IACvE,CAAC;AACD,QAAI,gBAAgB,WAAW,EAAG;AAClC,QAAI;AACF,YAAM,IAAI,OAAO,SAAS,EAAE,MAAM,QAAQ,WAAW,EAAE,KAAK,KAAK,EAAE,CAAC;AAAA,IACtE,QAAQ;AACN,qBAAe;AACf;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,qBAAqB,QAA8D;AACjG,MAAI,UAAU;AACd,MAAI,aAAgD;AAEpD,iBAAe,OAAsB;AACnC,WAAO,CAAC,SAAS;AACf,UAAI;AACF,cAAM,MAAM,MAAM,wBAAwB,MAAM;AAChD,qBAAa;AACb,YAAI,OAAO,mBAAoB,QAAO,mBAAmB,CAAC,GAAG,IAAI,KAAK,CAAC;AAEvE,cAAM,kBAAkB,KAAK;AAAA,UAC3B,uBAAuB,OAAO;AAAA,UAC9B,YAAY,MAAM;AAAA,QACpB,CAAC;AACD,YAAI,OAAO,aAAc,QAAO,aAAa;AAC7C,YAAI;AAAE,gBAAM,IAAI,MAAM;AAAA,QAAE,QAAQ;AAAA,QAAoB;AACpD,qBAAa;AAAA,MACf,QAAQ;AAAA,MAER;AACA,UAAI,QAAS;AACb,YAAM,OAAO,OAAO,oBAAoB;AACxC,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,IAAI,CAAC;AAAA,IAC5C;AAAA,EACF;AAEA,OAAK,KAAK;AAEV,SAAO;AAAA,IACL,MAAM,YAAY;AAChB,gBAAU;AACV,UAAI,YAAY;AACd,YAAI;AAAE,gBAAM,WAAW,MAAM;AAAA,QAAE,QAAQ;AAAA,QAAoB;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AACF;;;AFvKO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,iBAAiB,MAA2E;AAC1G,QAAM,UAAU;AAAA,IACd,qDAAqD,IAAI;AAAA,IACzD;AAAA,IACA;AAAA,IACA,sCAAsC,IAAI;AAAA,IAC1C;AAAA,EACF,EAAE,KAAK,GAAG;AACV,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,QAAQ,yBAAyB,MAAM,oBAAoB;AAAA,EACrE;AACF;AAEO,SAAS,aAAa,MAAyB,MAAyB,QAAQ,KAAc;AACnG,MAAI;AAEJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AACnB,UAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,oBAAY;AAAM;AAAK;AAAA,MACzB;AAGE;AAAA,IACJ;AAAA,EACF;AAEA,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,gBAAY,IAAI;AAAA,EAClB;AAEA,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,UAAU;AACrB;AAEA,eAAsB,KACpB,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAC9C,MAAyB,QAAQ,KAClB;AACf,MAAI;AACJ,MAAI;AACF,WAAO,aAAa,MAAM,GAAG;AAAA,EAC/B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAQ,OAAO,MAAM,4BAA4B,GAAG;AAAA,CAAI;AACxD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,OAAO,WAAW;AAExB,QAAM,aAAa,kBAAkB;AACrC,QAAM,iBAAiB,IAAI,qBAAqB;AAEhD,MAAI,4BAA4B;AAChC,QAAM,aAAa,qBAAqB;AAAA,IACtC,WAAW,KAAK;AAAA,IAChB,oBAAoB;AAAA,IACpB,qBAAqB,CAAC,WAAW;AAC/B,uBAAiB,YAAY,MAA2D;AAAA,IAC1F;AAAA,IACA,oBAAoB,MAAM;AACxB,kCAA4B;AAG5B,YAAM,OAAO,iBAAiB,IAAI;AAClC,uBAAiB,YAAY,IAAI;AAAA,IACnC;AAAA,EACF,CAAC;AAED,MAAI,UAAU;AACd,QAAM,WAAW,YAA2B;AAC1C,QAAI,QAAS;AACb,cAAU;AACV,QAAI;AAAE,YAAM,WAAW,KAAK;AAAA,IAAE,QAAQ;AAAA,IAAoB;AAC1D,QAAI;AAAE,YAAM,WAAW,MAAM;AAAA,IAAE,QAAQ;AAAA,IAAoB;AAC3D,QAAI,CAAC,2BAA2B;AAC9B,cAAQ,OAAO,MAAM,kDAAkD,KAAK,SAAS;AAAA,CAAI;AACzF,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,iBAAe,UAAU,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE;AAEjD,QAAM,WAAW,QAAQ,cAAc;AAEvC,UAAQ,GAAG,WAAW,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE,CAAC;AAC/C,UAAQ,GAAG,UAAU,MAAM;AAAE,SAAK,SAAS;AAAA,EAAE,CAAC;AAChD;AAMA,SAAS,UAAmB;AAC1B,MAAI;AACF,UAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,UAAM,WAAW,aAAa,QAAQ,KAAK,CAAC,CAAC;AAC7C,WAAO,aAAa;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,IAAI,QAAQ,GAAG;AACb,OAAK,KAAK;AACZ;","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -640,8 +640,10 @@ function identityKey(team, name) {
|
|
|
640
640
|
var RegisterAgentService = class {
|
|
641
641
|
repo;
|
|
642
642
|
connections = /* @__PURE__ */ new Map();
|
|
643
|
-
|
|
643
|
+
deps;
|
|
644
|
+
constructor(db, deps = {}) {
|
|
644
645
|
this.repo = new AgentsRepo(db);
|
|
646
|
+
this.deps = deps;
|
|
645
647
|
}
|
|
646
648
|
register(input) {
|
|
647
649
|
const validated = input.delivery === void 0 ? void 0 : validateDeliveryForWrite(input.delivery);
|
|
@@ -656,7 +658,22 @@ var RegisterAgentService = class {
|
|
|
656
658
|
});
|
|
657
659
|
const key = identityKey(team, input.name);
|
|
658
660
|
const bound = this.connections.get(key);
|
|
659
|
-
if (bound && bound !== input.connection_id)
|
|
661
|
+
if (bound && bound !== input.connection_id) {
|
|
662
|
+
let closed = false;
|
|
663
|
+
if (this.deps.closeSessionByConnectionId) {
|
|
664
|
+
try {
|
|
665
|
+
closed = this.deps.closeSessionByConnectionId(bound);
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const log = this.deps.log ?? ((line) => {
|
|
670
|
+
console.debug(line);
|
|
671
|
+
});
|
|
672
|
+
try {
|
|
673
|
+
log(`register_agent takeover: old=${bound} new=${input.connection_id} team=${team} name=${input.name} closed=${closed}`);
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
660
677
|
this.connections.set(key, input.connection_id);
|
|
661
678
|
return this.repo.register({
|
|
662
679
|
agent_type: input.agent_type,
|
|
@@ -3216,10 +3233,10 @@ function inferRuntimeAgentKind(args, clientInfo) {
|
|
|
3216
3233
|
if (raw.includes("opencode")) return "opencode";
|
|
3217
3234
|
return void 0;
|
|
3218
3235
|
}
|
|
3219
|
-
function registerBusinessTools(server, db, getCallerAgentId, fanout, onRegisterSuccess, getSessionId, channelWakeFanout, getTransport, getSessionClientInfo, onUnregisterSuccess) {
|
|
3236
|
+
function registerBusinessTools(server, db, getCallerAgentId, fanout, onRegisterSuccess, getSessionId, channelWakeFanout, getTransport, getSessionClientInfo, onUnregisterSuccess, injectedRegisterSvc) {
|
|
3220
3237
|
const agents = new AgentsRepo(db);
|
|
3221
3238
|
const events = new EventsOutbox(db);
|
|
3222
|
-
const registerSvc = new RegisterAgentService(db);
|
|
3239
|
+
const registerSvc = injectedRegisterSvc ?? new RegisterAgentService(db);
|
|
3223
3240
|
const bindRuntimeIdentitySvc = new BindRuntimeIdentityService(db);
|
|
3224
3241
|
const registerCodexSelfSvc = new RegisterCodexSelfService(registerSvc);
|
|
3225
3242
|
const unregisterSelfSvc = new UnregisterSelfService(db, agents);
|
|
@@ -3980,8 +3997,24 @@ function registerBusinessTools(server, db, getCallerAgentId, fanout, onRegisterS
|
|
|
3980
3997
|
}
|
|
3981
3998
|
|
|
3982
3999
|
// src/mcp/transport.ts
|
|
3983
|
-
function mountMcp(app, db, fanout, channelWakeFanout) {
|
|
4000
|
+
function mountMcp(app, db, fanout, channelWakeFanout, opts = {}) {
|
|
3984
4001
|
const sessions = /* @__PURE__ */ new Map();
|
|
4002
|
+
const log = opts.log ?? ((line) => {
|
|
4003
|
+
console.debug(line);
|
|
4004
|
+
});
|
|
4005
|
+
function closeSessionByConnectionId(connectionId) {
|
|
4006
|
+
const s = sessions.get(connectionId);
|
|
4007
|
+
if (!s) return false;
|
|
4008
|
+
try {
|
|
4009
|
+
void s.transport.close();
|
|
4010
|
+
} catch {
|
|
4011
|
+
}
|
|
4012
|
+
return true;
|
|
4013
|
+
}
|
|
4014
|
+
const registerSvc = new RegisterAgentService(db, {
|
|
4015
|
+
closeSessionByConnectionId,
|
|
4016
|
+
log
|
|
4017
|
+
});
|
|
3985
4018
|
const sessionOwners = /* @__PURE__ */ new Map();
|
|
3986
4019
|
function createSession() {
|
|
3987
4020
|
const server = new McpServer(
|
|
@@ -4046,7 +4079,14 @@ function mountMcp(app, db, fanout, channelWakeFanout) {
|
|
|
4046
4079
|
sessionIdGenerator: () => randomUUID6(),
|
|
4047
4080
|
onsessioninitialized: (sid) => {
|
|
4048
4081
|
sessionIdForCaller = sid;
|
|
4049
|
-
sessions.set(sid, {
|
|
4082
|
+
sessions.set(sid, {
|
|
4083
|
+
transport,
|
|
4084
|
+
server,
|
|
4085
|
+
sessionId: sid,
|
|
4086
|
+
agentIdHolder,
|
|
4087
|
+
createdAt: Date.now(),
|
|
4088
|
+
clientInfo: void 0
|
|
4089
|
+
});
|
|
4050
4090
|
}
|
|
4051
4091
|
});
|
|
4052
4092
|
transport.onclose = () => {
|
|
@@ -4063,6 +4103,12 @@ function mountMcp(app, db, fanout, channelWakeFanout) {
|
|
|
4063
4103
|
}
|
|
4064
4104
|
}
|
|
4065
4105
|
if (transport.sessionId) {
|
|
4106
|
+
if (agentIdHolder.current) {
|
|
4107
|
+
try {
|
|
4108
|
+
registerSvc.releaseConnection(agentIdHolder.current, transport.sessionId);
|
|
4109
|
+
} catch {
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4066
4112
|
sessions.delete(transport.sessionId);
|
|
4067
4113
|
sessionOwners.delete(transport.sessionId);
|
|
4068
4114
|
}
|
|
@@ -4081,10 +4127,11 @@ function mountMcp(app, db, fanout, channelWakeFanout) {
|
|
|
4081
4127
|
if (!sid) return void 0;
|
|
4082
4128
|
return sessions.get(sid)?.clientInfo;
|
|
4083
4129
|
},
|
|
4084
|
-
onUnregisterSuccess
|
|
4130
|
+
onUnregisterSuccess,
|
|
4131
|
+
registerSvc
|
|
4085
4132
|
);
|
|
4086
4133
|
server.connect(transport);
|
|
4087
|
-
return { transport, server, sessionId: "", agentIdHolder };
|
|
4134
|
+
return { transport, server, sessionId: "", agentIdHolder, createdAt: Date.now() };
|
|
4088
4135
|
}
|
|
4089
4136
|
function authHashFor(req) {
|
|
4090
4137
|
const raw = req.headers["authorization"];
|
|
@@ -4148,6 +4195,18 @@ function mountMcp(app, db, fanout, channelWakeFanout) {
|
|
|
4148
4195
|
await session.transport.handleRequest(req.raw, reply.raw);
|
|
4149
4196
|
return reply;
|
|
4150
4197
|
});
|
|
4198
|
+
function reapOrphanSessions(now, graceMs = 6e4) {
|
|
4199
|
+
for (const session of sessions.values()) {
|
|
4200
|
+
if (session.agentIdHolder.current !== void 0) continue;
|
|
4201
|
+
const ageMs = now - session.createdAt;
|
|
4202
|
+
if (ageMs < graceMs) continue;
|
|
4203
|
+
try {
|
|
4204
|
+
void session.transport.close();
|
|
4205
|
+
} catch {
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return { reapOrphanSessions };
|
|
4151
4210
|
}
|
|
4152
4211
|
|
|
4153
4212
|
// src/daemon/cleanup.ts
|
|
@@ -4302,6 +4361,7 @@ var ChannelWakeFanout = class {
|
|
|
4302
4361
|
|
|
4303
4362
|
// src/daemon/server.ts
|
|
4304
4363
|
var DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 12e4;
|
|
4364
|
+
var DEFAULT_ORPHAN_GC_INTERVAL_MS = 6e4;
|
|
4305
4365
|
function parsePositiveInt(raw, fallback) {
|
|
4306
4366
|
const n = Number(raw);
|
|
4307
4367
|
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
@@ -4318,7 +4378,7 @@ async function buildServer(opts) {
|
|
|
4318
4378
|
const channelWakeFanout = opts.channelWakeFanout ?? new ChannelWakeFanout();
|
|
4319
4379
|
app.addHook("onRequest", makeAuthHook(opts.token));
|
|
4320
4380
|
app.get("/health", async () => ({ ok: true, version, uptime_seconds: Math.floor((Date.now() - startedAt) / 1e3) }));
|
|
4321
|
-
mountMcp(app, db, fanout, channelWakeFanout);
|
|
4381
|
+
const mcp = mountMcp(app, db, fanout, channelWakeFanout);
|
|
4322
4382
|
const cleanupIntervalMs = opts.cleanupIntervalMs ?? Number(process.env.CLEANUP_INTERVAL_MS ?? 60 * 60 * 1e3);
|
|
4323
4383
|
const interval = setInterval(() => {
|
|
4324
4384
|
try {
|
|
@@ -4327,8 +4387,17 @@ async function buildServer(opts) {
|
|
|
4327
4387
|
}
|
|
4328
4388
|
}, cleanupIntervalMs);
|
|
4329
4389
|
if (typeof interval.unref === "function") interval.unref();
|
|
4390
|
+
const orphanGcIntervalMs = opts.orphanGcIntervalMs ?? parsePositiveInt(process.env.ORPHAN_GC_INTERVAL_MS, DEFAULT_ORPHAN_GC_INTERVAL_MS);
|
|
4391
|
+
const orphanGcInterval = setInterval(() => {
|
|
4392
|
+
try {
|
|
4393
|
+
mcp.reapOrphanSessions(Date.now());
|
|
4394
|
+
} catch {
|
|
4395
|
+
}
|
|
4396
|
+
}, orphanGcIntervalMs);
|
|
4397
|
+
if (typeof orphanGcInterval.unref === "function") orphanGcInterval.unref();
|
|
4330
4398
|
app.addHook("onClose", async () => {
|
|
4331
4399
|
clearInterval(interval);
|
|
4400
|
+
clearInterval(orphanGcInterval);
|
|
4332
4401
|
clearAllRetries();
|
|
4333
4402
|
fanout.stopAll();
|
|
4334
4403
|
db.close();
|