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.
Files changed (60) hide show
  1. package/README.md +106 -129
  2. package/README.zh.md +136 -105
  3. package/channel.ts +194 -431
  4. package/dist/auth.d.ts +10 -6
  5. package/dist/auth.js +32 -17
  6. package/dist/auth.js.map +1 -1
  7. package/dist/db.d.ts +32 -16
  8. package/dist/db.js +210 -113
  9. package/dist/db.js.map +1 -1
  10. package/dist/federation-http.d.ts +2 -0
  11. package/dist/federation-http.js +233 -0
  12. package/dist/federation-http.js.map +1 -0
  13. package/dist/index.js +82 -79
  14. package/dist/index.js.map +1 -1
  15. package/dist/log.d.ts +3 -0
  16. package/dist/log.js +10 -0
  17. package/dist/log.js.map +1 -0
  18. package/dist/mcp/agent-tools.d.ts +2 -0
  19. package/dist/mcp/agent-tools.js +66 -0
  20. package/dist/mcp/agent-tools.js.map +1 -0
  21. package/dist/mcp/dm-tools.d.ts +2 -0
  22. package/dist/mcp/dm-tools.js +54 -0
  23. package/dist/mcp/dm-tools.js.map +1 -0
  24. package/dist/mcp/federation-tools.d.ts +2 -0
  25. package/dist/mcp/federation-tools.js +30 -0
  26. package/dist/mcp/federation-tools.js.map +1 -0
  27. package/dist/mcp/server.d.ts +2 -0
  28. package/dist/mcp/server.js +62 -0
  29. package/dist/mcp/server.js.map +1 -0
  30. package/dist/mcp/task-tools.d.ts +2 -0
  31. package/dist/mcp/task-tools.js +128 -0
  32. package/dist/mcp/task-tools.js.map +1 -0
  33. package/dist/mcp/team-tools.d.ts +2 -0
  34. package/dist/mcp/team-tools.js +95 -0
  35. package/dist/mcp/team-tools.js.map +1 -0
  36. package/dist/models.d.ts +22 -10
  37. package/dist/models.js +1 -3
  38. package/dist/models.js.map +1 -1
  39. package/dist/server.d.ts +2 -3
  40. package/dist/server.js +38 -761
  41. package/dist/server.js.map +1 -1
  42. package/dist/sessions.d.ts +15 -0
  43. package/dist/sessions.js +107 -0
  44. package/dist/sessions.js.map +1 -0
  45. package/dist/tools/dm.d.ts +3 -2
  46. package/dist/tools/dm.js +14 -29
  47. package/dist/tools/dm.js.map +1 -1
  48. package/dist/tools/start.d.ts +3 -3
  49. package/dist/tools/start.js +18 -15
  50. package/dist/tools/start.js.map +1 -1
  51. package/dist/tools/task.d.ts +1 -1
  52. package/dist/tools/task.js +6 -5
  53. package/dist/tools/task.js.map +1 -1
  54. package/dist/tools/team.d.ts +62 -13
  55. package/dist/tools/team.js +102 -30
  56. package/dist/tools/team.js.map +1 -1
  57. package/package.json +2 -2
  58. package/dist/tools/room.d.ts +0 -39
  59. package/dist/tools/room.js +0 -35
  60. 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 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)
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
- if (sessionId) headers['Mcp-Session-Id'] = sessionId
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
- // Initialize MCP session with hive
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(name: string) {
86
- const result = await hiveCallTool('hive.start', { name, tool: 'claude', roles: 'channel' })
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
- // --- Channel MCP server ---
105
+ // --- MCP server (stdio, plugin-side) ---
93
106
 
94
107
  const mcp = new Server(
95
- { name: 'hive-channel', version: '0.1.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
- 'Messages arrive as <channel source="hive-channel" from="..." room_id="..." type="...">.',
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
- '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.',
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
- '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>/',
134
+ '## Artifacts',
135
+ '~/.kitty-hive/artifacts/<task_id>/',
115
136
  ].join('\n'),
116
137
  },
117
138
  )
118
139
 
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
- }
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
- 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
- }
145
+ interface MCPTool {
146
+ name: string
147
+ description?: string
148
+ inputSchema: any
149
+ }
367
150
 
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
- }
151
+ let cachedHiveTools: MCPTool[] = []
372
152
 
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
- }
153
+ function hiveToKebab(name: string): string {
154
+ return name.replace(/\./g, '-')
155
+ }
382
156
 
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
- }
157
+ function kebabToHive(name: string): string {
158
+ return name.replace(/-/g, '.')
159
+ }
387
160
 
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
- }
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
- if (name === 'hive-team-list') {
394
- const result = await hiveCallTool('hive.team.list', {})
395
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
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
- 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
- }
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
- if (name === 'hive-approve') {
409
- const result = await hiveCallTool('hive.workflow.approve', {
410
- as: agentName,
411
- task_id: args.task_id,
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
- 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
- }
204
+ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
205
+ const { name } = req.params
206
+ const args = (req.params.arguments || {}) as any
425
207
 
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) }] }
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
- if (name === 'hive-peers') {
437
- const result = await hiveCallTool('hive.peers', {})
438
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
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
- 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) }] }
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
- // --- 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
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(url, { method: 'GET', headers })
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
- // 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
- },
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
- await registerAgent(name)
578
- console.error(`[hive-channel] connected as "${agentName}" (${agentId})`)
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()