kitty-hive 0.1.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 +267 -0
- package/README.zh.md +213 -0
- package/channel.ts +590 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +19 -0
- package/dist/auth.js.map +1 -0
- package/dist/db.d.ts +63 -0
- package/dist/db.js +395 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +462 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +65 -0
- package/dist/models.js +10 -0
- package/dist/models.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +871 -0
- package/dist/server.js.map +1 -0
- package/dist/state-machine.d.ts +6 -0
- package/dist/state-machine.js +55 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/tools/dm.d.ts +12 -0
- package/dist/tools/dm.js +63 -0
- package/dist/tools/dm.js.map +1 -0
- package/dist/tools/room.d.ts +39 -0
- package/dist/tools/room.js +35 -0
- package/dist/tools/room.js.map +1 -0
- package/dist/tools/start.d.ts +16 -0
- package/dist/tools/start.js +54 -0
- package/dist/tools/start.js.map +1 -0
- package/dist/tools/task.d.ts +47 -0
- package/dist/tools/task.js +222 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/team.d.ts +25 -0
- package/dist/tools/team.js +44 -0
- package/dist/tools/team.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -0
- package/package.json +45 -0
package/channel.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* kitty-hive channel plugin for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Bridges kitty-hive MCP server events to Claude Code sessions via Channels.
|
|
6
|
+
* Polls hive inbox for new messages and pushes them as <channel> notifications.
|
|
7
|
+
* Exposes a reply tool so Claude can respond directly.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* claude --dangerously-load-development-channels server:hive-channel
|
|
11
|
+
*
|
|
12
|
+
* Requires kitty-hive server running (default: http://localhost:4123/mcp)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
17
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
18
|
+
|
|
19
|
+
const HIVE_URL = process.env.HIVE_URL || 'http://localhost:4123/mcp'
|
|
20
|
+
const HIVE_AGENT_NAME = process.env.HIVE_AGENT_NAME || ''
|
|
21
|
+
const POLL_INTERVAL = parseInt(process.env.HIVE_POLL_INTERVAL || '3000', 10)
|
|
22
|
+
|
|
23
|
+
// --- Hive HTTP client ---
|
|
24
|
+
|
|
25
|
+
let sessionId: string | null = null
|
|
26
|
+
let agentId: string | null = null
|
|
27
|
+
let agentName: string | null = null
|
|
28
|
+
let rpcId = 0
|
|
29
|
+
|
|
30
|
+
async function hivePost(method: string, params: any = {}) {
|
|
31
|
+
const headers: Record<string, string> = {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Accept': 'application/json, text/event-stream',
|
|
34
|
+
}
|
|
35
|
+
if (sessionId) headers['Mcp-Session-Id'] = sessionId
|
|
36
|
+
|
|
37
|
+
const res = await fetch(HIVE_URL, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: ++rpcId, method, params }),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const sid = res.headers.get('mcp-session-id')
|
|
44
|
+
if (sid) sessionId = sid
|
|
45
|
+
|
|
46
|
+
const text = await res.text()
|
|
47
|
+
for (const line of text.split('\n')) {
|
|
48
|
+
if (line.startsWith('data:')) {
|
|
49
|
+
const data = JSON.parse(line.slice(5))
|
|
50
|
+
if (data.error) throw new Error(JSON.stringify(data.error))
|
|
51
|
+
return data.result
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const data = JSON.parse(text)
|
|
55
|
+
if (data.error) throw new Error(JSON.stringify(data.error))
|
|
56
|
+
return data.result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function hiveCallTool(name: string, args: any = {}) {
|
|
60
|
+
const result = await hivePost('tools/call', { name, arguments: args })
|
|
61
|
+
const text = result.content[0].text
|
|
62
|
+
if (result.isError) throw new Error(text)
|
|
63
|
+
try { return JSON.parse(text) } catch { return text }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function initHiveSession() {
|
|
67
|
+
// Initialize MCP session with hive
|
|
68
|
+
await hivePost('initialize', {
|
|
69
|
+
protocolVersion: '2025-03-26',
|
|
70
|
+
capabilities: {},
|
|
71
|
+
clientInfo: { name: 'hive-channel', version: '1.0' },
|
|
72
|
+
})
|
|
73
|
+
// Send initialized notification
|
|
74
|
+
await fetch(HIVE_URL, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'Accept': 'application/json, text/event-stream',
|
|
79
|
+
'Mcp-Session-Id': sessionId!,
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function registerAgent(name: string) {
|
|
86
|
+
const result = await hiveCallTool('hive.start', { name, tool: 'claude', roles: 'channel' })
|
|
87
|
+
agentId = result.agent_id
|
|
88
|
+
agentName = result.display_name
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Channel MCP server ---
|
|
93
|
+
|
|
94
|
+
const mcp = new Server(
|
|
95
|
+
{ name: 'hive-channel', version: '0.1.0' },
|
|
96
|
+
{
|
|
97
|
+
capabilities: {
|
|
98
|
+
experimental: { 'claude/channel': {} },
|
|
99
|
+
tools: {},
|
|
100
|
+
},
|
|
101
|
+
instructions: [
|
|
102
|
+
'You are connected to kitty-hive, a multi-agent collaboration server.',
|
|
103
|
+
'Messages arrive as <channel source="hive-channel" from="..." room_id="..." type="...">.',
|
|
104
|
+
'',
|
|
105
|
+
'Communication: hive-dm (send DM), hive-inbox (check unread).',
|
|
106
|
+
'Teams: hive-team-create, hive-team-join (by name), hive-team-list.',
|
|
107
|
+
'Tasks: hive-task (create), hive-claim (claim unassigned), hive-tasks (list/board), hive-check (status).',
|
|
108
|
+
'Workflow: hive-propose (propose steps), hive-approve, hive-step-complete, hive-reject.',
|
|
109
|
+
'Federation: hive-peers (list peers), hive-remote-agents (list remote agents). Use agent@node for cross-node DM/task.',
|
|
110
|
+
'',
|
|
111
|
+
'IMPORTANT: When you receive a task, propose a workflow (hive-propose) before starting.',
|
|
112
|
+
'When you see an unassigned task, claim it with hive-claim.',
|
|
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>/',
|
|
115
|
+
].join('\n'),
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// --- Tools ---
|
|
120
|
+
|
|
121
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
122
|
+
tools: [
|
|
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
|
+
}
|
|
347
|
+
|
|
348
|
+
if (name === 'hive-claim') {
|
|
349
|
+
const result = await hiveCallTool('hive.task.claim', { as: agentName, task_id: args.task_id })
|
|
350
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
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
|
+
}
|
|
367
|
+
|
|
368
|
+
if (name === 'hive-room-info') {
|
|
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
|
+
}
|
|
372
|
+
|
|
373
|
+
if (name === 'hive-events') {
|
|
374
|
+
const result = await hiveCallTool('hive.room.events', {
|
|
375
|
+
as: agentName,
|
|
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
|
+
}
|
|
382
|
+
|
|
383
|
+
if (name === 'hive-team-create') {
|
|
384
|
+
const result = await hiveCallTool('hive.team.create', { as: agentName, name: args.name })
|
|
385
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (name === 'hive-team-join') {
|
|
389
|
+
const result = await hiveCallTool('hive.team.join', { as: agentName, room_id: args.room_id, name: args.name })
|
|
390
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (name === 'hive-team-list') {
|
|
394
|
+
const result = await hiveCallTool('hive.team.list', {})
|
|
395
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (name === 'hive-propose') {
|
|
399
|
+
const workflow = typeof args.workflow === 'string' ? JSON.parse(args.workflow) : args.workflow
|
|
400
|
+
const result = await hiveCallTool('hive.workflow.propose', {
|
|
401
|
+
as: agentName,
|
|
402
|
+
task_id: args.task_id,
|
|
403
|
+
workflow,
|
|
404
|
+
})
|
|
405
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (name === 'hive-approve') {
|
|
409
|
+
const result = await hiveCallTool('hive.workflow.approve', {
|
|
410
|
+
as: agentName,
|
|
411
|
+
task_id: args.task_id,
|
|
412
|
+
})
|
|
413
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (name === 'hive-step-complete') {
|
|
417
|
+
const result = await hiveCallTool('hive.workflow.step.complete', {
|
|
418
|
+
as: agentName,
|
|
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
|
+
}
|
|
425
|
+
|
|
426
|
+
if (name === 'hive-reject') {
|
|
427
|
+
const result = await hiveCallTool('hive.workflow.reject', {
|
|
428
|
+
as: agentName,
|
|
429
|
+
task_id: args.task_id,
|
|
430
|
+
step: Number(args.step),
|
|
431
|
+
reason: args.reason,
|
|
432
|
+
})
|
|
433
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (name === 'hive-peers') {
|
|
437
|
+
const result = await hiveCallTool('hive.peers', {})
|
|
438
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (name === 'hive-remote-agents') {
|
|
442
|
+
const result = await hiveCallTool('hive.remote.agents', { peer: args.peer })
|
|
443
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
throw new Error(`unknown tool: ${name}`)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// --- Dedup ---
|
|
450
|
+
const pushedMessages = new Set<string>();
|
|
451
|
+
function dedup(from: string, roomId: string, content: string): boolean {
|
|
452
|
+
const key = `${from}:${roomId}:${content.slice(0, 50)}`;
|
|
453
|
+
if (pushedMessages.has(key)) return false;
|
|
454
|
+
pushedMessages.add(key);
|
|
455
|
+
// Keep set bounded
|
|
456
|
+
if (pushedMessages.size > 500) {
|
|
457
|
+
const first = pushedMessages.values().next().value;
|
|
458
|
+
if (first) pushedMessages.delete(first);
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// --- SSE listener ---
|
|
464
|
+
|
|
465
|
+
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
|
+
while (true) {
|
|
473
|
+
try {
|
|
474
|
+
const res = await fetch(url, { method: 'GET', headers })
|
|
475
|
+
if (!res.ok || !res.body) {
|
|
476
|
+
console.error(`[hive-channel] SSE connect failed: ${res.status}`)
|
|
477
|
+
await new Promise(r => setTimeout(r, 3000))
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.error(`[hive-channel] SSE stream connected`)
|
|
482
|
+
const reader = res.body.getReader()
|
|
483
|
+
const decoder = new TextDecoder()
|
|
484
|
+
let buffer = ''
|
|
485
|
+
|
|
486
|
+
while (true) {
|
|
487
|
+
const { done, value } = await reader.read()
|
|
488
|
+
if (done) break
|
|
489
|
+
|
|
490
|
+
buffer += decoder.decode(value, { stream: true })
|
|
491
|
+
const lines = buffer.split('\n')
|
|
492
|
+
buffer = lines.pop() || ''
|
|
493
|
+
|
|
494
|
+
for (const line of lines) {
|
|
495
|
+
if (!line.startsWith('data:')) continue
|
|
496
|
+
try {
|
|
497
|
+
const data = JSON.parse(line.slice(5))
|
|
498
|
+
// logging notification from hive server
|
|
499
|
+
if (data.method === 'notifications/message' && data.params?.data) {
|
|
500
|
+
const raw = data.params.data
|
|
501
|
+
let parsed: any
|
|
502
|
+
try { parsed = JSON.parse(raw) } catch { parsed = { message: raw } }
|
|
503
|
+
|
|
504
|
+
// Skip if it's not a message-type event
|
|
505
|
+
if (!parsed.type || parsed.type === 'join' || parsed.type === 'leave') continue
|
|
506
|
+
|
|
507
|
+
const content = parsed.preview || parsed.title || raw;
|
|
508
|
+
const from = parsed.from || 'unknown';
|
|
509
|
+
const roomId = parsed.room_id || '';
|
|
510
|
+
if (!dedup(from, roomId, content)) continue;
|
|
511
|
+
|
|
512
|
+
await mcp.notification({
|
|
513
|
+
method: 'notifications/claude/channel',
|
|
514
|
+
params: {
|
|
515
|
+
content,
|
|
516
|
+
meta: {
|
|
517
|
+
from,
|
|
518
|
+
room_id: roomId,
|
|
519
|
+
room_name: parsed.room_name || '',
|
|
520
|
+
room_kind: parsed.room_kind || '',
|
|
521
|
+
type: parsed.type || 'message',
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
} catch { /* ignore parse errors */ }
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.error(`[hive-channel] SSE error, reconnecting...`, err)
|
|
531
|
+
}
|
|
532
|
+
// Reconnect after disconnect
|
|
533
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
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
|
+
// --- Start ---
|
|
568
|
+
|
|
569
|
+
await mcp.connect(new StdioServerTransport())
|
|
570
|
+
|
|
571
|
+
// Connect to hive with retry
|
|
572
|
+
async function connectToHive() {
|
|
573
|
+
const name = HIVE_AGENT_NAME || `channel-${Date.now().toString(36)}`
|
|
574
|
+
while (true) {
|
|
575
|
+
try {
|
|
576
|
+
await initHiveSession()
|
|
577
|
+
await registerAgent(name)
|
|
578
|
+
console.error(`[hive-channel] connected as "${agentName}" (${agentId})`)
|
|
579
|
+
return
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error(`[hive-channel] hive not ready, retrying in 3s...`)
|
|
582
|
+
await new Promise(r => setTimeout(r, 3000))
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await connectToHive()
|
|
588
|
+
|
|
589
|
+
// SSE only, no polling
|
|
590
|
+
listenSSE()
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getAgentByToken, touchAgent } from './db.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract agent from Bearer token.
|
|
4
|
+
* Returns the agent if valid, null otherwise.
|
|
5
|
+
* Also updates last_seen timestamp.
|
|
6
|
+
*/
|
|
7
|
+
export function authenticateToken(authHeader) {
|
|
8
|
+
if (!authHeader)
|
|
9
|
+
return null;
|
|
10
|
+
const match = authHeader.match(/^Bearer\s+(\S+)$/i);
|
|
11
|
+
if (!match)
|
|
12
|
+
return null;
|
|
13
|
+
const agent = getAgentByToken(match[1]);
|
|
14
|
+
if (!agent)
|
|
15
|
+
return null;
|
|
16
|
+
touchAgent(agent.id);
|
|
17
|
+
return agent;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAGtD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B;IAC9D,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACpD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { Agent, Room, RoomEvent, RoomEventType, Task, TaskEvent, TaskEventType } from './models.js';
|
|
3
|
+
export declare function initDB(dbPath?: string): Database.Database;
|
|
4
|
+
export declare function getDB(): Database.Database;
|
|
5
|
+
export declare function createAgent(displayName: string, tool: string, roles: string, expertise: string): Agent;
|
|
6
|
+
export declare function getAgentByToken(token: string): Agent | undefined;
|
|
7
|
+
export declare function getAgentById(id: string): Agent | undefined;
|
|
8
|
+
export declare function getAgentByName(name: string): Agent | undefined;
|
|
9
|
+
export declare function findAgentByRole(role: string): Agent | undefined;
|
|
10
|
+
export declare function touchAgent(id: string): void;
|
|
11
|
+
export declare function createRoom(kind: string, hostAgentId: string | null, name?: string): Room;
|
|
12
|
+
export declare function getRoomById(id: string): Room | undefined;
|
|
13
|
+
export declare function getLobby(): Room | undefined;
|
|
14
|
+
export declare function findDMRoom(agentA: string, agentB: string): Room | undefined;
|
|
15
|
+
export declare function listTeams(): Room[];
|
|
16
|
+
export declare function appendRoomEvent(roomId: string, type: RoomEventType, actorAgentId: string | null, payload?: object): RoomEvent;
|
|
17
|
+
export declare function getRoomEvents(roomId: string, since?: number, limit?: number): RoomEvent[];
|
|
18
|
+
export declare function getLastRoomEventTs(roomId: string): string | null;
|
|
19
|
+
export declare function getLatestRoomEvents(roomId: string, limit?: number): RoomEvent[];
|
|
20
|
+
export declare function getRoomMembers(roomId: string): string[];
|
|
21
|
+
export declare function isMember(roomId: string, agentId: string): boolean;
|
|
22
|
+
export declare function getAgentRooms(agentId: string, kind?: string, activeOnly?: boolean): Room[];
|
|
23
|
+
export declare function createTask(title: string, creatorId: string, assigneeId?: string, sourceRoomId?: string, input?: object): Task;
|
|
24
|
+
export declare function getTaskById(id: string): Task | undefined;
|
|
25
|
+
export declare function updateTaskStatus(id: string, status: string, extras?: Record<string, any>): void;
|
|
26
|
+
export declare function getAgentTasks(agentId: string, status?: string): Task[];
|
|
27
|
+
export declare function appendTaskEvent(taskId: string, type: TaskEventType, actorAgentId: string | null, payload?: object): TaskEvent;
|
|
28
|
+
export declare function getTaskEvents(taskId: string, since?: number, limit?: number): TaskEvent[];
|
|
29
|
+
export declare function getReadCursor(agentId: string, targetType: string, targetId: string): number;
|
|
30
|
+
export declare function setReadCursor(agentId: string, targetType: string, targetId: string, seq: number): void;
|
|
31
|
+
export interface UnreadSummary {
|
|
32
|
+
type: 'room' | 'task';
|
|
33
|
+
id: string;
|
|
34
|
+
name: string | null;
|
|
35
|
+
kind: string;
|
|
36
|
+
unread_count: number;
|
|
37
|
+
latest: Array<{
|
|
38
|
+
from: string;
|
|
39
|
+
type: string;
|
|
40
|
+
preview: string;
|
|
41
|
+
ts: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export declare function getUnreadForAgent(agentId: string): UnreadSummary[];
|
|
45
|
+
export interface Peer {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
url: string;
|
|
49
|
+
secret: string;
|
|
50
|
+
exposed: string;
|
|
51
|
+
status: string;
|
|
52
|
+
created_at: string;
|
|
53
|
+
last_seen: string | null;
|
|
54
|
+
}
|
|
55
|
+
export declare function addPeer(name: string, url: string, secret: string, exposed?: string): Peer;
|
|
56
|
+
export declare function getPeerByName(name: string): Peer | undefined;
|
|
57
|
+
export declare function getPeerBySecret(secret: string): Peer | undefined;
|
|
58
|
+
export declare function listPeers(): Peer[];
|
|
59
|
+
export declare function removePeer(name: string): boolean;
|
|
60
|
+
export declare function updatePeerExposed(name: string, exposed: string): void;
|
|
61
|
+
export declare function touchPeer(name: string): void;
|
|
62
|
+
export declare function isPeerExposed(peerName: string, agentName: string): boolean;
|
|
63
|
+
export declare function cleanupStaleTasks(maxAgeDays?: number): number;
|