kitty-hive 0.1.0 → 0.2.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 +106 -129
- package/README.zh.md +136 -105
- package/channel.ts +194 -431
- package/dist/auth.d.ts +10 -6
- package/dist/auth.js +32 -17
- package/dist/auth.js.map +1 -1
- package/dist/db.d.ts +32 -16
- package/dist/db.js +210 -113
- package/dist/db.js.map +1 -1
- package/dist/federation-http.d.ts +2 -0
- package/dist/federation-http.js +233 -0
- package/dist/federation-http.js.map +1 -0
- package/dist/index.js +82 -79
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +3 -0
- package/dist/log.js +10 -0
- package/dist/log.js.map +1 -0
- package/dist/mcp/agent-tools.d.ts +2 -0
- package/dist/mcp/agent-tools.js +66 -0
- package/dist/mcp/agent-tools.js.map +1 -0
- package/dist/mcp/dm-tools.d.ts +2 -0
- package/dist/mcp/dm-tools.js +54 -0
- package/dist/mcp/dm-tools.js.map +1 -0
- package/dist/mcp/federation-tools.d.ts +2 -0
- package/dist/mcp/federation-tools.js +30 -0
- package/dist/mcp/federation-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +62 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/task-tools.d.ts +2 -0
- package/dist/mcp/task-tools.js +128 -0
- package/dist/mcp/task-tools.js.map +1 -0
- package/dist/mcp/team-tools.d.ts +2 -0
- package/dist/mcp/team-tools.js +95 -0
- package/dist/mcp/team-tools.js.map +1 -0
- package/dist/models.d.ts +22 -10
- package/dist/models.js +1 -3
- package/dist/models.js.map +1 -1
- package/dist/server.d.ts +2 -3
- package/dist/server.js +38 -761
- package/dist/server.js.map +1 -1
- package/dist/sessions.d.ts +15 -0
- package/dist/sessions.js +107 -0
- package/dist/sessions.js.map +1 -0
- package/dist/tools/dm.d.ts +3 -2
- package/dist/tools/dm.js +14 -29
- package/dist/tools/dm.js.map +1 -1
- package/dist/tools/start.d.ts +3 -3
- package/dist/tools/start.js +18 -15
- package/dist/tools/start.js.map +1 -1
- package/dist/tools/task.d.ts +1 -1
- package/dist/tools/task.js +6 -5
- package/dist/tools/task.js.map +1 -1
- package/dist/tools/team.d.ts +62 -13
- package/dist/tools/team.js +102 -30
- package/dist/tools/team.js.map +1 -1
- package/package.json +2 -2
- package/dist/tools/room.d.ts +0 -39
- package/dist/tools/room.js +0 -35
- package/dist/tools/room.js.map +0 -1
package/channel.ts
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* kitty-hive channel plugin for Claude Code
|
|
4
4
|
*
|
|
5
|
-
* Bridges kitty-hive MCP server events to Claude Code
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* claude --dangerously-load-development-channels server:hive-channel
|
|
11
|
-
*
|
|
12
|
-
* Requires kitty-hive server running (default: http://localhost:4123/mcp)
|
|
5
|
+
* Bridges kitty-hive HTTP MCP server events to Claude Code via Channels.
|
|
6
|
+
* - Maintains an MCP session against the hive HTTP server
|
|
7
|
+
* - Listens to SSE for push notifications (DMs, team events, task events)
|
|
8
|
+
* - Forwards them as <channel> notifications into the Claude Code session
|
|
9
|
+
* - Re-exposes hive tools as `hive-*` (kebab-case) so Claude can call them directly
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
12
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
@@ -17,8 +14,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
17
14
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
18
15
|
|
|
19
16
|
const HIVE_URL = process.env.HIVE_URL || 'http://localhost:4123/mcp'
|
|
17
|
+
const HIVE_AGENT_ID = process.env.HIVE_AGENT_ID || ''
|
|
20
18
|
const HIVE_AGENT_NAME = process.env.HIVE_AGENT_NAME || ''
|
|
21
|
-
const POLL_INTERVAL = parseInt(process.env.HIVE_POLL_INTERVAL || '3000', 10)
|
|
22
19
|
|
|
23
20
|
// --- Hive HTTP client ---
|
|
24
21
|
|
|
@@ -26,20 +23,30 @@ let sessionId: string | null = null
|
|
|
26
23
|
let agentId: string | null = null
|
|
27
24
|
let agentName: string | null = null
|
|
28
25
|
let rpcId = 0
|
|
26
|
+
let sseStarted = false
|
|
29
27
|
|
|
30
|
-
async function hivePost(method: string, params: any = {}) {
|
|
28
|
+
async function hivePost(method: string, params: any = {}, _retried = false): Promise<any> {
|
|
31
29
|
const headers: Record<string, string> = {
|
|
32
30
|
'Content-Type': 'application/json',
|
|
33
31
|
'Accept': 'application/json, text/event-stream',
|
|
34
32
|
}
|
|
35
|
-
|
|
33
|
+
// initialize must not carry a session id (server creates one)
|
|
34
|
+
if (sessionId && method !== 'initialize') headers['Mcp-Session-Id'] = sessionId
|
|
36
35
|
|
|
37
36
|
const res = await fetch(HIVE_URL, {
|
|
38
|
-
method: 'POST',
|
|
39
|
-
headers,
|
|
37
|
+
method: 'POST', headers,
|
|
40
38
|
body: JSON.stringify({ jsonrpc: '2.0', id: ++rpcId, method, params }),
|
|
41
39
|
})
|
|
42
40
|
|
|
41
|
+
// Server lost our session (restarted) — re-init and retry once
|
|
42
|
+
if (res.status === 404 && !_retried && method !== 'initialize') {
|
|
43
|
+
console.error(`[hive-channel] server returned 404 (stale session); re-initializing...`)
|
|
44
|
+
sessionId = null
|
|
45
|
+
await initHiveSession()
|
|
46
|
+
if (agentId) await hiveCallTool('hive.start', { id: agentId, tool: 'claude', roles: 'channel' }, true)
|
|
47
|
+
return hivePost(method, params, true)
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
const sid = res.headers.get('mcp-session-id')
|
|
44
51
|
if (sid) sessionId = sid
|
|
45
52
|
|
|
@@ -56,21 +63,20 @@ async function hivePost(method: string, params: any = {}) {
|
|
|
56
63
|
return data.result
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
async function hiveCallTool(name: string, args: any = {}) {
|
|
60
|
-
const result = await hivePost('tools/call', { name, arguments: args })
|
|
66
|
+
async function hiveCallTool(name: string, args: any = {}, _retried = false) {
|
|
67
|
+
const result = await hivePost('tools/call', { name, arguments: args }, _retried)
|
|
61
68
|
const text = result.content[0].text
|
|
62
69
|
if (result.isError) throw new Error(text)
|
|
63
70
|
try { return JSON.parse(text) } catch { return text }
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
async function initHiveSession() {
|
|
67
|
-
//
|
|
74
|
+
sessionId = null // ensure no stale id is sent
|
|
68
75
|
await hivePost('initialize', {
|
|
69
76
|
protocolVersion: '2025-03-26',
|
|
70
77
|
capabilities: {},
|
|
71
78
|
clientInfo: { name: 'hive-channel', version: '1.0' },
|
|
72
79
|
})
|
|
73
|
-
// Send initialized notification
|
|
74
80
|
await fetch(HIVE_URL, {
|
|
75
81
|
method: 'POST',
|
|
76
82
|
headers: {
|
|
@@ -82,398 +88,188 @@ async function initHiveSession() {
|
|
|
82
88
|
})
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
async function registerAgent(
|
|
86
|
-
const
|
|
91
|
+
async function registerAgent(opts: { id?: string; name?: string }) {
|
|
92
|
+
const args: any = { tool: 'claude', roles: 'channel' }
|
|
93
|
+
if (opts.id) args.id = opts.id
|
|
94
|
+
if (opts.name) args.name = opts.name
|
|
95
|
+
const result = await hiveCallTool('hive.start', args)
|
|
87
96
|
agentId = result.agent_id
|
|
88
97
|
agentName = result.display_name
|
|
98
|
+
if (!sseStarted) {
|
|
99
|
+
sseStarted = true
|
|
100
|
+
listenSSE()
|
|
101
|
+
}
|
|
89
102
|
return result
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
// ---
|
|
105
|
+
// --- MCP server (stdio, plugin-side) ---
|
|
93
106
|
|
|
94
107
|
const mcp = new Server(
|
|
95
|
-
{ name: 'hive-channel', version: '0.
|
|
108
|
+
{ name: 'hive-channel', version: '0.2.0' },
|
|
96
109
|
{
|
|
97
|
-
capabilities: {
|
|
98
|
-
experimental: { 'claude/channel': {} },
|
|
99
|
-
tools: {},
|
|
100
|
-
},
|
|
110
|
+
capabilities: { experimental: { 'claude/channel': {} }, tools: {} },
|
|
101
111
|
instructions: [
|
|
102
112
|
'You are connected to kitty-hive, a multi-agent collaboration server.',
|
|
103
|
-
'
|
|
113
|
+
'Push notifications arrive as <channel source="hive-channel" from_agent_id="..." type="..."> blocks.',
|
|
114
|
+
'',
|
|
115
|
+
'## Identity',
|
|
116
|
+
'- IMPORTANT: On first use, ask the user "What name should I register on the hive?" and call hive-whoami(name=<that name>).',
|
|
117
|
+
'- Your agent_id (returned by whoami) is the stable handle for cross-team addressing.',
|
|
118
|
+
'- display_name is for display only; not unique. Per-team you can also have a unique nickname (hive-team-nickname).',
|
|
119
|
+
'',
|
|
120
|
+
'## Tools',
|
|
121
|
+
'- Identity: hive-whoami, hive-rename, hive-agents (list all agents on the hive)',
|
|
122
|
+
'- DM: hive-dm (to=agent id or team-nickname), hive-inbox',
|
|
123
|
+
'- Teams: hive-team-create, hive-team-join, hive-team-list, hive-teams (mine), hive-team-info, hive-team-events, hive-team-message, hive-team-nickname',
|
|
124
|
+
'- Tasks: hive-task, hive-claim, hive-tasks, hive-check',
|
|
125
|
+
'- Workflow: hive-propose, hive-approve, hive-step-complete, hive-reject',
|
|
126
|
+
'- Federation: hive-peers, hive-remote-agents (use id@node)',
|
|
104
127
|
'',
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
128
|
+
'## Workflow rules',
|
|
129
|
+
'- When you receive a task, propose a workflow (hive-propose) before starting.',
|
|
130
|
+
'- NEVER auto-approve a workflow — show the proposal to the user and wait for explicit confirmation.',
|
|
131
|
+
'- Mark each step with hive-step-complete.',
|
|
132
|
+
'- Claim unassigned tasks with hive-claim.',
|
|
110
133
|
'',
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'NEVER auto-approve a workflow — always show the proposal to the user and wait for explicit confirmation before calling hive-approve.',
|
|
114
|
-
'Artifacts: ~/.kitty-hive/artifacts/<task_id>/',
|
|
134
|
+
'## Artifacts',
|
|
135
|
+
'~/.kitty-hive/artifacts/<task_id>/',
|
|
115
136
|
].join('\n'),
|
|
116
137
|
},
|
|
117
138
|
)
|
|
118
139
|
|
|
119
|
-
// ---
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
name: 'hive-dm',
|
|
125
|
-
description: 'Send a direct message to another agent on kitty-hive',
|
|
126
|
-
inputSchema: {
|
|
127
|
-
type: 'object',
|
|
128
|
-
properties: {
|
|
129
|
-
to: { type: 'string', description: 'Target agent name or ID' },
|
|
130
|
-
content: { type: 'string', description: 'Message content' },
|
|
131
|
-
},
|
|
132
|
-
required: ['to', 'content'],
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: 'hive-inbox',
|
|
137
|
-
description: 'Check all unread messages on kitty-hive',
|
|
138
|
-
inputSchema: { type: 'object', properties: {} },
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: 'hive-task',
|
|
142
|
-
description: 'Create a task and delegate to an agent or role.',
|
|
143
|
-
inputSchema: {
|
|
144
|
-
type: 'object',
|
|
145
|
-
properties: {
|
|
146
|
-
to: { type: 'string', description: 'Target: agent name or "role:ux"' },
|
|
147
|
-
title: { type: 'string', description: 'Task title' },
|
|
148
|
-
input: { type: 'object', description: 'Structured task input (description, output_path, output_format)' },
|
|
149
|
-
},
|
|
150
|
-
required: ['title'],
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: 'hive-claim',
|
|
155
|
-
description: 'Claim an unassigned task',
|
|
156
|
-
inputSchema: {
|
|
157
|
-
type: 'object',
|
|
158
|
-
properties: { task_id: { type: 'string', description: 'Task ID' } },
|
|
159
|
-
required: ['task_id'],
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: 'hive-tasks',
|
|
164
|
-
description: 'List your tasks (created or assigned), grouped by status',
|
|
165
|
-
inputSchema: {
|
|
166
|
-
type: 'object',
|
|
167
|
-
properties: { status: { type: 'string', description: 'Filter: created, in_progress, completed, etc.' } },
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: 'hive-check',
|
|
172
|
-
description: 'Check the current state of a task by task ID',
|
|
173
|
-
inputSchema: {
|
|
174
|
-
type: 'object',
|
|
175
|
-
properties: {
|
|
176
|
-
task_id: { type: 'string', description: 'Task ID to check' },
|
|
177
|
-
},
|
|
178
|
-
required: ['task_id'],
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: 'hive-rooms',
|
|
183
|
-
description: 'List rooms you are a member of',
|
|
184
|
-
inputSchema: {
|
|
185
|
-
type: 'object',
|
|
186
|
-
properties: {
|
|
187
|
-
kind: { type: 'string', description: 'Filter by room kind: dm, team, lobby' },
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: 'hive-room-info',
|
|
193
|
-
description: 'Get detailed info about a room including members and recent events',
|
|
194
|
-
inputSchema: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: {
|
|
197
|
-
room_id: { type: 'string', description: 'Room ID' },
|
|
198
|
-
},
|
|
199
|
-
required: ['room_id'],
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
name: 'hive-events',
|
|
204
|
-
description: 'Fetch events from a room. Use "since" for incremental polling.',
|
|
205
|
-
inputSchema: {
|
|
206
|
-
type: 'object',
|
|
207
|
-
properties: {
|
|
208
|
-
room_id: { type: 'string', description: 'Room ID' },
|
|
209
|
-
since: { type: 'number', description: 'Return events after this seq number' },
|
|
210
|
-
limit: { type: 'number', description: 'Max events to return (default 50)' },
|
|
211
|
-
},
|
|
212
|
-
required: ['room_id'],
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
name: 'hive-team-create',
|
|
217
|
-
description: 'Create a team room for group collaboration',
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: { name: { type: 'string', description: 'Team name' } },
|
|
221
|
-
required: ['name'],
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
name: 'hive-team-join',
|
|
226
|
-
description: 'Join an existing team room by name or ID',
|
|
227
|
-
inputSchema: {
|
|
228
|
-
type: 'object',
|
|
229
|
-
properties: {
|
|
230
|
-
room_id: { type: 'string', description: 'Team room ID' },
|
|
231
|
-
name: { type: 'string', description: 'Team name' },
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
name: 'hive-team-list',
|
|
237
|
-
description: 'List all available teams',
|
|
238
|
-
inputSchema: { type: 'object', properties: {} },
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
name: 'hive-propose',
|
|
242
|
-
description: 'Propose a workflow for a task. Define steps with assignees, actions, and completion criteria.',
|
|
243
|
-
inputSchema: {
|
|
244
|
-
type: 'object',
|
|
245
|
-
properties: {
|
|
246
|
-
task_id: { type: 'string', description: 'Task ID' },
|
|
247
|
-
workflow: {
|
|
248
|
-
type: 'array',
|
|
249
|
-
description: 'Workflow steps',
|
|
250
|
-
items: {
|
|
251
|
-
type: 'object',
|
|
252
|
-
properties: {
|
|
253
|
-
step: { type: 'number' },
|
|
254
|
-
title: { type: 'string' },
|
|
255
|
-
assignees: { type: 'array', items: { type: 'string' }, description: 'Agent names or "role:xxx"' },
|
|
256
|
-
action: { type: 'string' },
|
|
257
|
-
completion: { type: 'string', description: '"all" or "any"' },
|
|
258
|
-
on_reject: { type: 'string', description: '"revise" or "back:N"' },
|
|
259
|
-
},
|
|
260
|
-
required: ['step', 'title', 'assignees', 'action'],
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
required: ['task_id', 'workflow'],
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
name: 'hive-approve',
|
|
269
|
-
description: 'Approve a proposed workflow. Automatically starts step 1.',
|
|
270
|
-
inputSchema: {
|
|
271
|
-
type: 'object',
|
|
272
|
-
properties: { task_id: { type: 'string', description: 'Task ID' } },
|
|
273
|
-
required: ['task_id'],
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
name: 'hive-step-complete',
|
|
278
|
-
description: 'Mark your part of the current step as complete.',
|
|
279
|
-
inputSchema: {
|
|
280
|
-
type: 'object',
|
|
281
|
-
properties: {
|
|
282
|
-
task_id: { type: 'string', description: 'Task ID' },
|
|
283
|
-
step: { type: 'number', description: 'Step number' },
|
|
284
|
-
result: { type: 'string', description: 'Result description' },
|
|
285
|
-
},
|
|
286
|
-
required: ['task_id', 'step'],
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: 'hive-reject',
|
|
291
|
-
description: 'Reject the current step. Sends the task back to a previous step.',
|
|
292
|
-
inputSchema: {
|
|
293
|
-
type: 'object',
|
|
294
|
-
properties: {
|
|
295
|
-
task_id: { type: 'string', description: 'Task ID' },
|
|
296
|
-
step: { type: 'number', description: 'Step being rejected' },
|
|
297
|
-
reason: { type: 'string', description: 'Rejection reason' },
|
|
298
|
-
},
|
|
299
|
-
required: ['task_id', 'step'],
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
name: 'hive-peers',
|
|
304
|
-
description: 'List connected federation peers',
|
|
305
|
-
inputSchema: { type: 'object', properties: {} },
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
name: 'hive-remote-agents',
|
|
309
|
-
description: 'List agents on a remote peer node',
|
|
310
|
-
inputSchema: {
|
|
311
|
-
type: 'object',
|
|
312
|
-
properties: { peer: { type: 'string', description: 'Peer node name' } },
|
|
313
|
-
required: ['peer'],
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
],
|
|
317
|
-
}))
|
|
318
|
-
|
|
319
|
-
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
320
|
-
const { name } = req.params
|
|
321
|
-
const args = req.params.arguments as Record<string, string>
|
|
322
|
-
|
|
323
|
-
if (name === 'hive-dm') {
|
|
324
|
-
const result = await hiveCallTool('hive.dm', {
|
|
325
|
-
as: agentName,
|
|
326
|
-
to: args.to,
|
|
327
|
-
content: args.content,
|
|
328
|
-
})
|
|
329
|
-
return { content: [{ type: 'text', text: `sent (room: ${result.room_id})` }] }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (name === 'hive-inbox') {
|
|
333
|
-
const result = await hiveCallTool('hive.inbox', { as: agentName })
|
|
334
|
-
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] }
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (name === 'hive-task') {
|
|
338
|
-
const input = args.input ? (typeof args.input === 'string' ? JSON.parse(args.input) : args.input) : undefined
|
|
339
|
-
const result = await hiveCallTool('hive.task', {
|
|
340
|
-
as: agentName,
|
|
341
|
-
to: args.to,
|
|
342
|
-
title: args.title,
|
|
343
|
-
input,
|
|
344
|
-
})
|
|
345
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
346
|
-
}
|
|
140
|
+
// --- Dynamic tool proxy ---
|
|
141
|
+
// Channel discovers hive.* tools from the HTTP server at startup, then exposes
|
|
142
|
+
// them as kebab-case `hive-*` tools. Calls are forwarded with `as: agentId`
|
|
143
|
+
// injected. Only `hive-whoami` is implemented locally (manages session state).
|
|
347
144
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (name === 'hive-tasks') {
|
|
354
|
-
const result = await hiveCallTool('hive.tasks', { as: agentName, status: args.status })
|
|
355
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (name === 'hive-check') {
|
|
359
|
-
const result = await hiveCallTool('hive.check', { task_id: args.task_id })
|
|
360
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (name === 'hive-rooms') {
|
|
364
|
-
const result = await hiveCallTool('hive.room.list', { as: agentName, kind: args.kind })
|
|
365
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
366
|
-
}
|
|
145
|
+
interface MCPTool {
|
|
146
|
+
name: string
|
|
147
|
+
description?: string
|
|
148
|
+
inputSchema: any
|
|
149
|
+
}
|
|
367
150
|
|
|
368
|
-
|
|
369
|
-
const result = await hiveCallTool('hive.room.info', { as: agentName, room_id: args.room_id })
|
|
370
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
371
|
-
}
|
|
151
|
+
let cachedHiveTools: MCPTool[] = []
|
|
372
152
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
room_id: args.room_id,
|
|
377
|
-
since: args.since ? Number(args.since) : undefined,
|
|
378
|
-
limit: args.limit ? Number(args.limit) : undefined,
|
|
379
|
-
})
|
|
380
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
381
|
-
}
|
|
153
|
+
function hiveToKebab(name: string): string {
|
|
154
|
+
return name.replace(/\./g, '-')
|
|
155
|
+
}
|
|
382
156
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
157
|
+
function kebabToHive(name: string): string {
|
|
158
|
+
return name.replace(/-/g, '.')
|
|
159
|
+
}
|
|
387
160
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
161
|
+
function stripAsParam(schema: any): any {
|
|
162
|
+
if (!schema?.properties) return schema
|
|
163
|
+
const { as: _as, ...rest } = schema.properties
|
|
164
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((r: string) => r !== 'as') : undefined
|
|
165
|
+
const out: any = { ...schema, properties: rest }
|
|
166
|
+
if (required && required.length > 0) out.required = required
|
|
167
|
+
else delete out.required
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
392
170
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
171
|
+
async function refreshHiveTools(): Promise<void> {
|
|
172
|
+
try {
|
|
173
|
+
const result = await hivePost('tools/list', {})
|
|
174
|
+
cachedHiveTools = (result.tools || []).filter((t: MCPTool) => t.name.startsWith('hive.'))
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('[hive-channel] failed to fetch tool list:', err)
|
|
396
177
|
}
|
|
178
|
+
}
|
|
397
179
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
180
|
+
const WHOAMI_TOOL: MCPTool = {
|
|
181
|
+
name: 'hive-whoami',
|
|
182
|
+
description: 'Show your agent id, display_name, and registration info. If not registered, pass `name` to register.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: { name: { type: 'string', description: 'Agent name (only when first registering)' } },
|
|
186
|
+
},
|
|
187
|
+
}
|
|
407
188
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
189
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
190
|
+
if (cachedHiveTools.length === 0) await refreshHiveTools()
|
|
191
|
+
const tools: MCPTool[] = [WHOAMI_TOOL]
|
|
192
|
+
for (const t of cachedHiveTools) {
|
|
193
|
+
// hive.whoami is served locally (manages registration state)
|
|
194
|
+
if (t.name === 'hive.whoami') continue
|
|
195
|
+
tools.push({
|
|
196
|
+
name: hiveToKebab(t.name),
|
|
197
|
+
description: t.description,
|
|
198
|
+
inputSchema: stripAsParam(t.inputSchema),
|
|
412
199
|
})
|
|
413
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
414
200
|
}
|
|
201
|
+
return { tools }
|
|
202
|
+
})
|
|
415
203
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
task_id: args.task_id,
|
|
420
|
-
step: Number(args.step),
|
|
421
|
-
result: args.result,
|
|
422
|
-
})
|
|
423
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
424
|
-
}
|
|
204
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
205
|
+
const { name } = req.params
|
|
206
|
+
const args = (req.params.arguments || {}) as any
|
|
425
207
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
208
|
+
// Local handler: hive-whoami
|
|
209
|
+
if (name === 'hive-whoami') {
|
|
210
|
+
if (!agentName) {
|
|
211
|
+
if (!args.name) {
|
|
212
|
+
return { content: [{ type: 'text', text: 'Not registered. Provide a "name" parameter to register.' }], isError: true }
|
|
213
|
+
}
|
|
214
|
+
await registerAgent({ name: args.name })
|
|
215
|
+
await refreshHiveTools()
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: JSON.stringify({ agent_id: agentId, agent_name: agentName, hive_url: HIVE_URL, session_id: sessionId }, null, 2),
|
|
221
|
+
}],
|
|
222
|
+
}
|
|
434
223
|
}
|
|
435
224
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return {
|
|
225
|
+
// Lazy-registration guard
|
|
226
|
+
if (!agentName) {
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: 'text', text: 'Not registered. Call hive-whoami(name=<your-agent-name>) first.' }],
|
|
229
|
+
isError: true,
|
|
230
|
+
}
|
|
439
231
|
}
|
|
440
232
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
233
|
+
// Proxy all other hive-* tools to hive.* with `as: agentId` injected
|
|
234
|
+
if (!name.startsWith('hive-')) throw new Error(`Unknown tool: ${name}`)
|
|
235
|
+
const hiveName = kebabToHive(name)
|
|
236
|
+
const result = await hiveCallTool(hiveName, { as: agentId, ...args })
|
|
237
|
+
return {
|
|
238
|
+
content: [{
|
|
239
|
+
type: 'text',
|
|
240
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
241
|
+
}],
|
|
444
242
|
}
|
|
445
|
-
|
|
446
|
-
throw new Error(`unknown tool: ${name}`)
|
|
447
243
|
})
|
|
448
244
|
|
|
449
|
-
// ---
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (pushedMessages.has(key)) return false
|
|
454
|
-
pushedMessages.add(key)
|
|
455
|
-
// Keep set bounded
|
|
245
|
+
// --- Push notifications: SSE → channel ---
|
|
246
|
+
|
|
247
|
+
const pushedMessages = new Set<string>()
|
|
248
|
+
function dedup(key: string): boolean {
|
|
249
|
+
if (pushedMessages.has(key)) return false
|
|
250
|
+
pushedMessages.add(key)
|
|
456
251
|
if (pushedMessages.size > 500) {
|
|
457
|
-
const first = pushedMessages.values().next().value
|
|
458
|
-
if (first) pushedMessages.delete(first)
|
|
252
|
+
const first = pushedMessages.values().next().value
|
|
253
|
+
if (first) pushedMessages.delete(first)
|
|
459
254
|
}
|
|
460
|
-
return true
|
|
255
|
+
return true
|
|
461
256
|
}
|
|
462
257
|
|
|
463
|
-
// --- SSE listener ---
|
|
464
|
-
|
|
465
258
|
async function listenSSE() {
|
|
466
|
-
const url = HIVE_URL
|
|
467
|
-
const headers: Record<string, string> = {
|
|
468
|
-
'Accept': 'text/event-stream',
|
|
469
|
-
'Mcp-Session-Id': sessionId!,
|
|
470
|
-
}
|
|
471
|
-
|
|
472
259
|
while (true) {
|
|
473
260
|
try {
|
|
474
|
-
const res = await fetch(
|
|
261
|
+
const res = await fetch(HIVE_URL, {
|
|
262
|
+
method: 'GET',
|
|
263
|
+
headers: { 'Accept': 'text/event-stream', 'Mcp-Session-Id': sessionId! },
|
|
264
|
+
})
|
|
475
265
|
if (!res.ok || !res.body) {
|
|
476
|
-
console.error(`[hive-channel] SSE connect failed: ${res.status}
|
|
266
|
+
console.error(`[hive-channel] SSE connect failed: ${res.status}, re-registering...`)
|
|
267
|
+
try {
|
|
268
|
+
await initHiveSession()
|
|
269
|
+
if (agentId) await hiveCallTool('hive.start', { id: agentId, tool: 'claude', roles: 'channel' })
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error(`[hive-channel] re-register failed:`, e)
|
|
272
|
+
}
|
|
477
273
|
await new Promise(r => setTimeout(r, 3000))
|
|
478
274
|
continue
|
|
479
275
|
}
|
|
@@ -495,96 +291,63 @@ async function listenSSE() {
|
|
|
495
291
|
if (!line.startsWith('data:')) continue
|
|
496
292
|
try {
|
|
497
293
|
const data = JSON.parse(line.slice(5))
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
},
|
|
294
|
+
if (data.method !== 'notifications/message' || !data.params?.data) continue
|
|
295
|
+
|
|
296
|
+
const raw = data.params.data
|
|
297
|
+
let parsed: any
|
|
298
|
+
try { parsed = JSON.parse(raw) } catch { parsed = { type: 'message', preview: raw } }
|
|
299
|
+
|
|
300
|
+
// Skip join/leave noise
|
|
301
|
+
if (parsed.type === 'join' || parsed.type === 'leave') continue
|
|
302
|
+
|
|
303
|
+
const content = parsed.preview || parsed.title || raw
|
|
304
|
+
const from = parsed.from || parsed.from_agent_id || 'unknown'
|
|
305
|
+
const key = `${from}:${parsed.type}:${content.slice(0, 60)}`
|
|
306
|
+
if (!dedup(key)) continue
|
|
307
|
+
|
|
308
|
+
await mcp.notification({
|
|
309
|
+
method: 'notifications/claude/channel',
|
|
310
|
+
params: {
|
|
311
|
+
content,
|
|
312
|
+
meta: {
|
|
313
|
+
type: parsed.type,
|
|
314
|
+
from,
|
|
315
|
+
from_agent_id: parsed.from_agent_id || '',
|
|
316
|
+
team_id: parsed.team_id || '',
|
|
317
|
+
task_id: parsed.task_id || '',
|
|
523
318
|
},
|
|
524
|
-
}
|
|
525
|
-
}
|
|
319
|
+
},
|
|
320
|
+
})
|
|
526
321
|
} catch { /* ignore parse errors */ }
|
|
527
322
|
}
|
|
528
323
|
}
|
|
529
324
|
} catch (err) {
|
|
530
325
|
console.error(`[hive-channel] SSE error, reconnecting...`, err)
|
|
531
326
|
}
|
|
532
|
-
// Reconnect after disconnect
|
|
533
327
|
await new Promise(r => setTimeout(r, 2000))
|
|
534
328
|
}
|
|
535
329
|
}
|
|
536
330
|
|
|
537
|
-
// --- Fallback: poll inbox on startup to catch missed messages ---
|
|
538
|
-
|
|
539
|
-
async function drainInbox() {
|
|
540
|
-
try {
|
|
541
|
-
const unread = await hiveCallTool('hive.inbox', { as: agentName })
|
|
542
|
-
if (!Array.isArray(unread)) return
|
|
543
|
-
|
|
544
|
-
for (const room of unread) {
|
|
545
|
-
for (const msg of room.latest) {
|
|
546
|
-
if (msg.type === 'join' || msg.type === 'leave') continue
|
|
547
|
-
const content = msg.preview || `[${msg.type} event]`;
|
|
548
|
-
if (!dedup(msg.from, room.id, content)) continue;
|
|
549
|
-
await mcp.notification({
|
|
550
|
-
method: 'notifications/claude/channel',
|
|
551
|
-
params: {
|
|
552
|
-
content,
|
|
553
|
-
meta: {
|
|
554
|
-
from: msg.from,
|
|
555
|
-
room_id: room.id,
|
|
556
|
-
room_name: room.name || '',
|
|
557
|
-
room_kind: room.kind,
|
|
558
|
-
type: msg.type,
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
})
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
} catch { /* ignore */ }
|
|
565
|
-
}
|
|
566
|
-
|
|
567
331
|
// --- Start ---
|
|
568
332
|
|
|
569
333
|
await mcp.connect(new StdioServerTransport())
|
|
570
334
|
|
|
571
|
-
// Connect to hive with retry
|
|
572
335
|
async function connectToHive() {
|
|
573
|
-
const name = HIVE_AGENT_NAME || `channel-${Date.now().toString(36)}`
|
|
574
336
|
while (true) {
|
|
575
337
|
try {
|
|
576
338
|
await initHiveSession()
|
|
577
|
-
|
|
578
|
-
|
|
339
|
+
if (HIVE_AGENT_ID || HIVE_AGENT_NAME) {
|
|
340
|
+
await registerAgent({ id: HIVE_AGENT_ID || undefined, name: HIVE_AGENT_NAME || undefined })
|
|
341
|
+
console.error(`[hive-channel] connected as "${agentName}" (${agentId})`)
|
|
342
|
+
} else {
|
|
343
|
+
console.error(`[hive-channel] connected (no env identity — register via hive-whoami)`)
|
|
344
|
+
}
|
|
579
345
|
return
|
|
580
346
|
} catch (err) {
|
|
581
|
-
console.error(`[hive-channel] hive not ready, retrying in 3s
|
|
347
|
+
console.error(`[hive-channel] hive not ready, retrying in 3s...`, err)
|
|
582
348
|
await new Promise(r => setTimeout(r, 3000))
|
|
583
349
|
}
|
|
584
350
|
}
|
|
585
351
|
}
|
|
586
352
|
|
|
587
353
|
await connectToHive()
|
|
588
|
-
|
|
589
|
-
// SSE only, no polling
|
|
590
|
-
listenSSE()
|