kitty-hive 0.1.1 β†’ 0.3.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 (63) hide show
  1. package/README.md +121 -126
  2. package/README.zh.md +153 -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 +47 -17
  8. package/dist/db.js +268 -123
  9. package/dist/db.js.map +1 -1
  10. package/dist/federation-heartbeat.d.ts +7 -0
  11. package/dist/federation-heartbeat.js +45 -0
  12. package/dist/federation-heartbeat.js.map +1 -0
  13. package/dist/federation-http.d.ts +7 -0
  14. package/dist/federation-http.js +287 -0
  15. package/dist/federation-http.js.map +1 -0
  16. package/dist/index.js +336 -114
  17. package/dist/index.js.map +1 -1
  18. package/dist/log.d.ts +3 -0
  19. package/dist/log.js +10 -0
  20. package/dist/log.js.map +1 -0
  21. package/dist/mcp/agent-tools.d.ts +2 -0
  22. package/dist/mcp/agent-tools.js +66 -0
  23. package/dist/mcp/agent-tools.js.map +1 -0
  24. package/dist/mcp/dm-tools.d.ts +2 -0
  25. package/dist/mcp/dm-tools.js +54 -0
  26. package/dist/mcp/dm-tools.js.map +1 -0
  27. package/dist/mcp/federation-tools.d.ts +2 -0
  28. package/dist/mcp/federation-tools.js +44 -0
  29. package/dist/mcp/federation-tools.js.map +1 -0
  30. package/dist/mcp/server.d.ts +2 -0
  31. package/dist/mcp/server.js +62 -0
  32. package/dist/mcp/server.js.map +1 -0
  33. package/dist/mcp/task-tools.d.ts +2 -0
  34. package/dist/mcp/task-tools.js +128 -0
  35. package/dist/mcp/task-tools.js.map +1 -0
  36. package/dist/mcp/team-tools.d.ts +2 -0
  37. package/dist/mcp/team-tools.js +95 -0
  38. package/dist/mcp/team-tools.js.map +1 -0
  39. package/dist/models.d.ts +28 -10
  40. package/dist/models.js +1 -3
  41. package/dist/models.js.map +1 -1
  42. package/dist/server.d.ts +2 -3
  43. package/dist/server.js +56 -761
  44. package/dist/server.js.map +1 -1
  45. package/dist/sessions.d.ts +15 -0
  46. package/dist/sessions.js +107 -0
  47. package/dist/sessions.js.map +1 -0
  48. package/dist/tools/dm.d.ts +3 -2
  49. package/dist/tools/dm.js +35 -32
  50. package/dist/tools/dm.js.map +1 -1
  51. package/dist/tools/start.d.ts +3 -3
  52. package/dist/tools/start.js +18 -15
  53. package/dist/tools/start.js.map +1 -1
  54. package/dist/tools/task.d.ts +7 -2
  55. package/dist/tools/task.js +113 -15
  56. package/dist/tools/task.js.map +1 -1
  57. package/dist/tools/team.d.ts +62 -13
  58. package/dist/tools/team.js +102 -30
  59. package/dist/tools/team.js.map +1 -1
  60. package/package.json +4 -3
  61. package/dist/tools/room.d.ts +0 -39
  62. package/dist/tools/room.js +0 -35
  63. package/dist/tools/room.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { startServer, setLogLevel } from './server.js';
3
- import { initDB, addPeer, listPeers, removePeer, updatePeerExposed, getPeerByName } from './db.js';
3
+ import { initDB, addPeer, listPeers, removePeer, updatePeerExposed, getPeerByName, setPeerNodeName, setPeerStatus, touchPeer } from './db.js';
4
+ import { pingPeer } from './federation-heartbeat.js';
4
5
  import { generateToken } from './utils.js';
5
6
  import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs';
6
- import { join, dirname } from 'node:path';
7
+ import { join, dirname, basename, delimiter } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
- import { homedir } from 'node:os';
9
+ import { homedir, hostname } from 'node:os';
9
10
  import { createInterface } from 'node:readline';
11
+ import { execSync } from 'node:child_process';
10
12
  const __filename = fileURLToPath(import.meta.url);
11
13
  const __dirname = dirname(__filename);
12
14
  const args = process.argv.slice(2);
@@ -45,124 +47,260 @@ async function cmdServe() {
45
47
  setLogLevel('warn');
46
48
  await startServer(port, dbPath);
47
49
  }
48
- async function cmdInit() {
49
- console.log('🐝 kitty-hive init\n');
50
- // Parse flags first
51
- let explicitPort = false;
52
- let explicitHttp = false;
53
- let agentName = '';
54
- let port = 4123;
55
- for (let i = 1; i < args.length; i++) {
56
- if (args[i] === '--http') {
57
- explicitHttp = true;
58
- }
59
- else if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
60
- port = parseInt(args[i + 1], 10);
61
- explicitPort = true;
62
- i++;
63
- }
64
- else if (args[i] === '--db') {
65
- i++;
66
- }
67
- else if (!args[i].startsWith('-')) {
68
- agentName = args[i];
69
- }
70
- }
71
- // Interactive: ask what wasn't provided
72
- if (!agentName) {
73
- agentName = await ask('Agent name', getDefaultAgentName());
50
+ const INIT_TOOLS = ['claude', 'cursor', 'vscode'];
51
+ const ALL_INIT_TARGETS = ['claude', 'cursor', 'vscode', 'antigravity'];
52
+ function findNpx() {
53
+ const probe = process.platform === 'win32' ? 'where npx' : 'command -v npx';
54
+ try {
55
+ const out = execSync(probe, { encoding: 'utf8' }).trim().split(/\r?\n/)[0];
56
+ return out || 'npx';
74
57
  }
75
- if (!explicitPort) {
76
- const p = await ask('Hive server port', '4123');
77
- port = parseInt(p, 10) || 4123;
58
+ catch {
59
+ return 'npx';
78
60
  }
79
- let useChannel = !explicitHttp;
80
- if (!explicitHttp) {
81
- const mode = await ask('Mode: (c)hannel push or (h)ttp pull?', 'c');
82
- useChannel = mode.toLowerCase() !== 'h';
61
+ }
62
+ function readJson(path) {
63
+ if (!existsSync(path))
64
+ return {};
65
+ try {
66
+ return JSON.parse(readFileSync(path, 'utf8'));
83
67
  }
84
- const channelPath = join(__dirname, '..', 'channel.ts');
85
- const mcpJsonPath = join(process.cwd(), '.mcp.json');
86
- let existing = {};
87
- if (existsSync(mcpJsonPath)) {
88
- try {
89
- existing = JSON.parse(readFileSync(mcpJsonPath, 'utf8'));
90
- }
91
- catch { /* ignore */ }
68
+ catch {
69
+ return {};
92
70
  }
93
- if (!existing.mcpServers)
94
- existing.mcpServers = {};
95
- if (useChannel) {
96
- existing.mcpServers['hive-channel'] = {
97
- command: 'npx',
98
- args: ['tsx', channelPath],
99
- env: {
100
- HIVE_URL: `http://localhost:${port}/mcp`,
101
- HIVE_AGENT_NAME: agentName,
71
+ }
72
+ function writeJson(path, data) {
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
75
+ }
76
+ function writeForTool(tool, port) {
77
+ const url = `http://localhost:${port}/mcp`;
78
+ const cwd = process.cwd();
79
+ if (tool === 'claude') {
80
+ const p = join(cwd, '.mcp.json');
81
+ const data = readJson(p);
82
+ if (!data.mcpServers)
83
+ data.mcpServers = {};
84
+ data.mcpServers['hive'] = { url };
85
+ delete data.mcpServers['hive-channel'];
86
+ writeJson(p, data);
87
+ return p;
88
+ }
89
+ if (tool === 'cursor') {
90
+ const p = join(cwd, '.cursor', 'mcp.json');
91
+ const data = readJson(p);
92
+ if (!data.mcpServers)
93
+ data.mcpServers = {};
94
+ data.mcpServers['hive'] = { url };
95
+ writeJson(p, data);
96
+ return p;
97
+ }
98
+ if (tool === 'vscode') {
99
+ const p = join(cwd, '.vscode', 'mcp.json');
100
+ const data = readJson(p);
101
+ if (!data.servers)
102
+ data.servers = {};
103
+ data.servers['hive'] = { type: 'http', url };
104
+ writeJson(p, data);
105
+ return p;
106
+ }
107
+ throw new Error(`Unknown tool: ${tool}`);
108
+ }
109
+ function antigravitySnippet(port) {
110
+ // Antigravity has no public on-disk config path β€” users edit via
111
+ // "..." β†’ MCP Store β†’ Manage MCP Servers β†’ View raw config.
112
+ // It also doesn't speak streamable HTTP directly, so we route through a stdio→HTTP adapter.
113
+ const url = `http://localhost:${port}/mcp`;
114
+ // Pre-seed PATH so the GUI app can find npx even when launched without a shell.
115
+ const pathDirs = process.platform === 'win32'
116
+ ? [
117
+ process.env.SystemRoot ? join(process.env.SystemRoot, 'System32') : 'C:\\Windows\\System32',
118
+ process.env.APPDATA ? join(process.env.APPDATA, 'npm') : '',
119
+ ].filter(Boolean)
120
+ : ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin'];
121
+ return JSON.stringify({
122
+ mcpServers: {
123
+ hive: {
124
+ command: findNpx(),
125
+ args: ['-y', '@pyroprompts/mcp-stdio-to-streamable-http-adapter'],
126
+ env: {
127
+ PATH: pathDirs.join(delimiter),
128
+ URI: url,
129
+ },
102
130
  },
103
- };
104
- delete existing.mcpServers['hive'];
131
+ },
132
+ }, null, 2);
133
+ }
134
+ function showInitUsage() {
135
+ console.log('🐝 kitty-hive init β€” write MCP config for an IDE\n');
136
+ console.log('Usage:');
137
+ console.log(' kitty-hive init <tool> [--port 4123]\n');
138
+ console.log('Tools:');
139
+ console.log(' claude .mcp.json (Claude Code β€” prefer the plugin instead)');
140
+ console.log(' cursor .cursor/mcp.json');
141
+ console.log(' vscode .vscode/mcp.json (VS Code Copilot)');
142
+ console.log(' antigravity prints snippet to paste via MCP Store UI');
143
+ console.log(' all run all of the above');
144
+ }
145
+ async function cmdInit() {
146
+ const tool = args[1];
147
+ if (!tool || tool.startsWith('-')) {
148
+ showInitUsage();
149
+ process.exit(1);
150
+ }
151
+ const known = ALL_INIT_TARGETS;
152
+ let targets;
153
+ if (tool === 'all') {
154
+ targets = [...ALL_INIT_TARGETS];
155
+ }
156
+ else if (known.includes(tool)) {
157
+ targets = [tool];
105
158
  }
106
159
  else {
107
- existing.mcpServers['hive'] = {
108
- url: `http://localhost:${port}/mcp`,
109
- };
110
- delete existing.mcpServers['hive-channel'];
160
+ console.log(`Unknown tool: "${tool}"`);
161
+ showInitUsage();
162
+ process.exit(1);
111
163
  }
112
- writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
113
- console.log(`\n🐝 kitty-hive configured`);
114
- console.log(` Agent: ${agentName}`);
115
- console.log(` Server: http://localhost:${port}/mcp`);
116
- console.log(` Mode: ${useChannel ? 'channel (push)' : 'HTTP (pull)'}`);
117
- if (useChannel) {
118
- console.log(`\n Run: claude --dangerously-load-development-channels server:hive-channel`);
164
+ let port = 4123;
165
+ for (let i = 2; i < args.length; i++) {
166
+ if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
167
+ port = parseInt(args[i + 1], 10) || 4123;
168
+ i++;
169
+ }
119
170
  }
171
+ console.log(`🐝 Configuring hive β†’ http://localhost:${port}/mcp\n`);
172
+ for (const t of targets) {
173
+ if (t === 'antigravity') {
174
+ console.log(` antigravity (no on-disk path β€” paste this snippet)`);
175
+ console.log(` Open: "..." β†’ MCP Store β†’ Manage MCP Servers β†’ View raw config`);
176
+ console.log(` Merge "hive" into mcpServers:\n`);
177
+ const snippet = antigravitySnippet(port).split('\n').map(l => ' ' + l).join('\n');
178
+ console.log(snippet);
179
+ console.log('');
180
+ }
181
+ else {
182
+ const path = writeForTool(t, port);
183
+ console.log(` ${t.padEnd(12)} ${path}`);
184
+ }
185
+ }
186
+ console.log(`\n Agent registers via hive.start when first used.`);
187
+ }
188
+ function relativeTime(iso) {
189
+ if (!iso)
190
+ return '-';
191
+ const then = new Date(iso).getTime();
192
+ if (Number.isNaN(then))
193
+ return iso;
194
+ const diff = Math.max(0, Date.now() - then);
195
+ const s = Math.floor(diff / 1000);
196
+ if (s < 5)
197
+ return 'now';
198
+ if (s < 60)
199
+ return `${s}s ago`;
200
+ const m = Math.floor(s / 60);
201
+ if (m < 60)
202
+ return `${m}m ago`;
203
+ const h = Math.floor(m / 60);
204
+ if (h < 24)
205
+ return `${h}h ago`;
206
+ const d = Math.floor(h / 24);
207
+ return `${d}d ago`;
208
+ }
209
+ function visualWidth(s) {
210
+ let w = 0;
211
+ for (const c of s) {
212
+ const cp = c.codePointAt(0);
213
+ const wide = (cp >= 0x1100 && cp <= 0x115F) || (cp >= 0x2E80 && cp <= 0x303E) ||
214
+ (cp >= 0x3041 && cp <= 0x33FF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
215
+ (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xA000 && cp <= 0xA4CF) ||
216
+ (cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) ||
217
+ (cp >= 0xFE30 && cp <= 0xFE4F) || (cp >= 0xFF00 && cp <= 0xFF60) ||
218
+ (cp >= 0xFFE0 && cp <= 0xFFE6) || (cp >= 0x20000 && cp <= 0x3FFFD);
219
+ w += wide ? 2 : 1;
220
+ }
221
+ return w;
222
+ }
223
+ function padCell(s, width) {
224
+ return s + ' '.repeat(Math.max(0, width - visualWidth(s)));
120
225
  }
226
+ function renderTable(headers, rows, indent = '') {
227
+ const widths = headers.map((h, i) => Math.max(visualWidth(h), ...rows.map(r => visualWidth(r[i] ?? ''))));
228
+ const fmt = (cells) => indent + cells.map((c, i) => padCell(c ?? '', widths[i])).join(' ').trimEnd();
229
+ const sep = indent + widths.map(w => '─'.repeat(w)).join(' ');
230
+ return [fmt(headers), sep, ...rows.map(fmt)].join('\n');
231
+ }
232
+ function agentRows(db, agents) {
233
+ const teamStmt = db.prepare(`
234
+ SELECT t.name, tm.nickname FROM teams t
235
+ JOIN team_members tm ON tm.team_id = t.id
236
+ WHERE tm.agent_id = ? AND t.closed_at IS NULL
237
+ ORDER BY t.name
238
+ `);
239
+ return agents.map(a => {
240
+ const memberTeams = teamStmt.all(a.id);
241
+ const teamLabels = memberTeams.map(t => t.nickname ? `${t.name}:${t.nickname}` : t.name).join(', ') || '-';
242
+ return [
243
+ a.id.slice(0, 12),
244
+ a.display_name,
245
+ a.tool || '-',
246
+ a.status,
247
+ a.roles || '-',
248
+ teamLabels,
249
+ relativeTime(a.last_seen),
250
+ ];
251
+ });
252
+ }
253
+ const AGENT_HEADERS = ['ID', 'NAME', 'TOOL', 'STATUS', 'ROLES', 'TEAMS', 'LAST SEEN'];
121
254
  async function cmdStatus() {
122
255
  const { port, dbPath } = parseFlags(1);
123
256
  const url = `http://localhost:${port}/mcp`;
124
- // Check server
257
+ const nodeName = getNodeConfig().name || hostname().split('.')[0];
125
258
  try {
126
- const res = await fetch(url, { method: 'GET' });
259
+ await fetch(url, { method: 'GET' });
127
260
  console.log(`🐝 Server: http://localhost:${port}/mcp (online)`);
128
261
  }
129
262
  catch {
130
263
  console.log(`❌ Server: http://localhost:${port}/mcp (offline)`);
131
264
  process.exit(1);
132
265
  }
133
- // Check DB
266
+ console.log(` Node: ${nodeName}`);
134
267
  try {
135
268
  const db = initDB(dbPath);
136
- const agents = db.prepare('SELECT id, display_name, status, last_seen FROM agents ORDER BY last_seen DESC').all();
137
- const rooms = db.prepare("SELECT id, name, kind FROM rooms WHERE closed_at IS NULL").all();
269
+ const agents = db.prepare("SELECT id, display_name, tool, roles, status, last_seen FROM agents WHERE origin_peer = '' ORDER BY last_seen DESC").all();
270
+ const remotes = db.prepare("SELECT id, display_name, tool, roles, status, last_seen, origin_peer FROM agents WHERE origin_peer != '' ORDER BY last_seen DESC").all();
271
+ const teams = db.prepare("SELECT id, name FROM teams WHERE closed_at IS NULL ORDER BY name").all();
138
272
  const tasks = db.prepare("SELECT count(*) as cnt FROM tasks WHERE status NOT IN ('completed','failed','canceled')").get();
139
- console.log(`\nπŸ“Š Database: ${dbPath || '~/.kitty-hive/hive.db'}`);
140
- console.log(` Rooms: ${rooms.length} Active tasks: ${tasks.cnt}`);
141
- console.log(`\nπŸ‘₯ Agents (${agents.length}):`);
142
- for (const a of agents) {
143
- // Find rooms this agent is in
144
- const memberRooms = db.prepare(`
145
- SELECT DISTINCT r.name FROM rooms r
146
- JOIN room_events e ON e.room_id = r.id AND e.type = 'join' AND e.actor_agent_id = ?
147
- WHERE r.closed_at IS NULL AND NOT EXISTS (
148
- SELECT 1 FROM room_events e2 WHERE e2.room_id = r.id AND e2.type = 'leave' AND e2.actor_agent_id = ? AND e2.seq > e.seq
149
- )
150
- `).all(a.id, a.id);
151
- const roomNames = memberRooms.map((r) => r.name).filter(Boolean).join(', ');
152
- console.log(` ${a.display_name} (${a.status}) β€” ${roomNames || 'no rooms'}`);
273
+ const peers = listPeers();
274
+ console.log(`πŸ“Š Database: ${dbPath || '~/.kitty-hive/hive.db'}`);
275
+ console.log(` ${agents.length} local agents Β· ${remotes.length} remote Β· ${teams.length} teams Β· ${tasks.cnt} active tasks Β· ${peers.length} peers`);
276
+ if (agents.length > 0) {
277
+ console.log(`\nπŸ‘₯ Agents`);
278
+ console.log(renderTable(AGENT_HEADERS, agentRows(db, agents), ' '));
153
279
  }
154
- if (rooms.length > 0) {
155
- console.log(`\n🏠 Rooms:`);
156
- for (const r of rooms) {
157
- const memberCount = db.prepare(`
158
- SELECT COUNT(DISTINCT e.actor_agent_id) as cnt FROM room_events e
159
- WHERE e.room_id = ? AND e.type = 'join'
160
- `).get(r.id);
161
- console.log(` ${r.name || r.id} (${r.kind}) β€” ${memberCount.cnt} members`);
280
+ if (teams.length > 0) {
281
+ console.log(`\n🏠 Teams`);
282
+ const rows = teams.map(t => {
283
+ const cnt = db.prepare('SELECT COUNT(*) as cnt FROM team_members WHERE team_id = ?').get(t.id).cnt;
284
+ return [t.name, String(cnt)];
285
+ });
286
+ console.log(renderTable(['NAME', 'MEMBERS'], rows, ' '));
287
+ }
288
+ if (peers.length > 0) {
289
+ console.log(`\n🀝 Peers`);
290
+ const rows = peers.map(p => [
291
+ p.name, p.node_name || '-', p.status, p.exposed || '-', relativeTime(p.last_seen),
292
+ ]);
293
+ console.log(renderTable(['NAME', 'NODE', 'STATUS', 'EXPOSED', 'LAST SEEN'], rows, ' '));
294
+ if (remotes.length > 0) {
295
+ console.log(`\n🌐 Remote agents (placeholders)`);
296
+ const rrows = remotes.map(a => [
297
+ a.id.slice(0, 12), a.display_name, a.origin_peer, a.status, relativeTime(a.last_seen),
298
+ ]);
299
+ console.log(renderTable(['ID', 'NAME', 'PEER', 'STATUS', 'LAST SEEN'], rrows, ' '));
162
300
  }
163
301
  }
164
302
  }
165
- catch (err) {
303
+ catch {
166
304
  console.log(`\n⚠️ Cannot read database`);
167
305
  }
168
306
  }
@@ -187,40 +325,80 @@ async function cmdDbClear() {
187
325
  }
188
326
  async function cmdAgentRemove() {
189
327
  const { dbPath } = parseFlags(1);
190
- const name = args[2];
191
- if (!name) {
192
- console.log('Usage: kitty-hive agent remove <name>');
328
+ const target = args[2];
329
+ if (!target) {
330
+ console.log('Usage: kitty-hive agent remove <name-or-id>');
193
331
  process.exit(1);
194
332
  }
195
333
  const db = initDB(dbPath);
196
- const agent = db.prepare('SELECT id, display_name FROM agents WHERE display_name = ?').get(name);
197
- if (!agent) {
198
- console.log(`Agent "${name}" not found.`);
334
+ const matches = db.prepare('SELECT id, display_name FROM agents WHERE id = ? OR display_name = ?').all(target, target);
335
+ if (matches.length === 0) {
336
+ console.log(`Agent "${target}" not found.`);
199
337
  process.exit(1);
200
338
  }
339
+ if (matches.length > 1) {
340
+ console.log(`"${target}" matches ${matches.length} agents. Use id to disambiguate.`);
341
+ for (const m of matches)
342
+ console.log(` ${m.id} ${m.display_name}`);
343
+ process.exit(1);
344
+ }
345
+ const agent = matches[0];
346
+ const name = agent.display_name;
201
347
  const confirm = await ask(`Remove agent "${name}" and all related data? (y/n)`, 'n');
202
348
  if (confirm.toLowerCase() !== 'y') {
203
349
  console.log('Cancelled.');
204
350
  process.exit(0);
205
351
  }
352
+ // Delete in dependency order to satisfy foreign keys
206
353
  db.prepare('DELETE FROM read_cursors WHERE agent_id = ?').run(agent.id);
354
+ // Tasks
355
+ db.prepare(`DELETE FROM task_events WHERE task_id IN (SELECT id FROM tasks WHERE creator_agent_id = ?)`).run(agent.id);
207
356
  db.prepare('DELETE FROM task_events WHERE actor_agent_id = ?').run(agent.id);
208
- db.prepare('DELETE FROM room_events WHERE actor_agent_id = ?').run(agent.id);
357
+ db.prepare('DELETE FROM tasks WHERE creator_agent_id = ?').run(agent.id);
209
358
  db.prepare('UPDATE tasks SET assignee_agent_id = NULL WHERE assignee_agent_id = ?').run(agent.id);
359
+ // DMs
360
+ db.prepare('DELETE FROM dm_messages WHERE from_agent_id = ? OR to_agent_id = ?').run(agent.id, agent.id);
361
+ // Team membership + events
362
+ db.prepare('DELETE FROM team_members WHERE agent_id = ?').run(agent.id);
363
+ db.prepare('DELETE FROM team_events WHERE actor_agent_id = ?').run(agent.id);
364
+ // Teams hosted by this agent (and their content)
365
+ db.prepare(`DELETE FROM team_events WHERE team_id IN (SELECT id FROM teams WHERE host_agent_id = ?)`).run(agent.id);
366
+ db.prepare(`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE host_agent_id = ?)`).run(agent.id);
367
+ db.prepare(`DELETE FROM read_cursors WHERE target_id IN (SELECT id FROM teams WHERE host_agent_id = ?)`).run(agent.id);
368
+ db.prepare('DELETE FROM teams WHERE host_agent_id = ?').run(agent.id);
210
369
  db.prepare('DELETE FROM agents WHERE id = ?').run(agent.id);
211
370
  console.log(`βœ… Removed agent "${name}".`);
212
371
  }
372
+ async function cmdAgentRename() {
373
+ const { dbPath } = parseFlags(1);
374
+ const oldName = args[2];
375
+ const newName = args[3];
376
+ if (!oldName || !newName) {
377
+ console.log('Usage: kitty-hive agent rename <old-name> <new-name>');
378
+ process.exit(1);
379
+ }
380
+ const db = initDB(dbPath);
381
+ const matches = db.prepare('SELECT id FROM agents WHERE display_name = ? OR id = ?').all(oldName, oldName);
382
+ if (matches.length === 0) {
383
+ console.log(`Agent "${oldName}" not found.`);
384
+ process.exit(1);
385
+ }
386
+ if (matches.length > 1) {
387
+ console.log(`"${oldName}" matches ${matches.length} agents. Use agent id to disambiguate.`);
388
+ process.exit(1);
389
+ }
390
+ db.prepare('UPDATE agents SET display_name = ? WHERE id = ?').run(newName, matches[0].id);
391
+ console.log(`βœ… Renamed "${oldName}" β†’ "${newName}".`);
392
+ }
213
393
  async function cmdAgentList() {
214
394
  const { dbPath } = parseFlags(1);
215
395
  const db = initDB(dbPath);
216
- const agents = db.prepare('SELECT display_name, roles, status, last_seen FROM agents ORDER BY last_seen DESC').all();
396
+ const agents = db.prepare('SELECT id, display_name, tool, roles, status, last_seen FROM agents ORDER BY last_seen DESC').all();
217
397
  if (agents.length === 0) {
218
398
  console.log('No agents registered.');
219
399
  return;
220
400
  }
221
- for (const a of agents) {
222
- console.log(` ${a.display_name} (${a.status}) roles=${a.roles || 'none'} last_seen=${a.last_seen}`);
223
- }
401
+ console.log(renderTable(AGENT_HEADERS, agentRows(db, agents)));
224
402
  }
225
403
  // --- Node config ---
226
404
  function getConfigPath() {
@@ -244,7 +422,7 @@ function setNodeConfig(config) {
244
422
  writeFileSync(p, JSON.stringify({ ...existing, ...config }, null, 2) + '\n');
245
423
  }
246
424
  function getNodeName() {
247
- return getNodeConfig().name || require('os').hostname().split('.')[0];
425
+ return getNodeConfig().name || hostname().split('.')[0];
248
426
  }
249
427
  // --- Peer commands ---
250
428
  async function cmdPeerAdd() {
@@ -292,6 +470,21 @@ async function cmdPeerAdd() {
292
470
  console.log(` URL: ${peerUrl}`);
293
471
  console.log(` Secret: ${secret}`);
294
472
  console.log(` Exposed agents: ${exposed || 'none (use --expose to add)'}`);
473
+ // Verify reachability via /federation/ping
474
+ process.stdout.write(` Pinging…`);
475
+ const result = await pingPeer(peerName, peerUrl, secret, 5000);
476
+ if (result.ok) {
477
+ if (result.node)
478
+ setPeerNodeName(peerName, result.node);
479
+ setPeerStatus(peerName, 'active');
480
+ touchPeer(peerName);
481
+ console.log(` ok (node="${result.node}")`);
482
+ }
483
+ else {
484
+ setPeerStatus(peerName, 'inactive');
485
+ console.log(` failed: ${result.error}`);
486
+ console.log(` (peer record kept; will retry on next heartbeat once server is reachable)`);
487
+ }
295
488
  console.log(`\n Give this secret to the peer so they can connect back.`);
296
489
  }
297
490
  async function cmdPeerList() {
@@ -302,11 +495,15 @@ async function cmdPeerList() {
302
495
  console.log('No peers configured.');
303
496
  return;
304
497
  }
305
- for (const p of peers) {
306
- console.log(` ${p.name} (${p.status}) β€” ${p.url}`);
307
- console.log(` exposed: ${p.exposed || 'none'}`);
308
- console.log(` last seen: ${p.last_seen || 'never'}`);
309
- }
498
+ const rows = peers.map(p => [
499
+ p.name,
500
+ p.node_name || '-',
501
+ p.status,
502
+ p.url,
503
+ p.exposed || '-',
504
+ relativeTime(p.last_seen),
505
+ ]);
506
+ console.log(renderTable(['NAME', 'NODE', 'STATUS', 'URL', 'EXPOSED', 'LAST SEEN'], rows));
310
507
  }
311
508
  async function cmdPeerRemove() {
312
509
  const { dbPath } = parseFlags(1);
@@ -358,6 +555,18 @@ async function cmdPeerExpose() {
358
555
  updatePeerExposed(peerName, current.join(','));
359
556
  console.log(`βœ… Peer "${peerName}" exposed agents: ${current.join(', ') || 'none'}`);
360
557
  }
558
+ async function cmdFilesClean() {
559
+ let maxAgeDays = 7;
560
+ for (let i = 2; i < args.length; i++) {
561
+ if ((args[i] === '--days' || args[i] === '-d') && args[i + 1]) {
562
+ maxAgeDays = parseInt(args[i + 1], 10) || 7;
563
+ i++;
564
+ }
565
+ }
566
+ const { cleanupOldFiles } = await import('./federation-http.js');
567
+ const result = cleanupOldFiles(maxAgeDays);
568
+ console.log(`βœ… Removed ${result.removed} federation file(s) older than ${maxAgeDays} day(s); kept ${result.kept}.`);
569
+ }
361
570
  async function cmdConfigSet() {
362
571
  // kitty-hive config set name marvin
363
572
  const key = args[2];
@@ -382,23 +591,25 @@ function getDefaultAgentName() {
382
591
  catch { /* ignore */ }
383
592
  }
384
593
  // Fallback to directory name
385
- return process.cwd().split('/').pop() || 'agent';
594
+ return basename(process.cwd()) || 'agent';
386
595
  }
387
596
  function showHelp() {
388
597
  console.log(`🐝 kitty-hive β€” multi-agent collaboration server
389
598
 
390
599
  Usage:
391
600
  kitty-hive serve [--port 4123] [--db path] [-v|-q] Start the server
392
- kitty-hive init [name] [--port 4123] [--http] Configure for this project
601
+ kitty-hive init <tool> [--port 4123] Write MCP config (claude|cursor|vscode|antigravity|all)
393
602
  kitty-hive status [--port 4123] Server & agent status
394
603
  kitty-hive agent list List agents
604
+ kitty-hive agent rename <old> <new> Rename an agent
395
605
  kitty-hive agent remove <name> Remove an agent
396
606
  kitty-hive peer add <name> <url> [--expose a,b] [--secret s] Add a peer
397
607
  kitty-hive peer list List peers
398
608
  kitty-hive peer remove <name> Remove a peer
399
609
  kitty-hive peer expose <name> --add/--remove <agent> Manage exposed agents
400
610
  kitty-hive config set <key> <value> Set config (e.g. name)
401
- kitty-hive db clear [--db path] Clear the database`);
611
+ kitty-hive db clear [--db path] Clear the database
612
+ kitty-hive files clean [--days 7] Remove old federation transfer files`);
402
613
  }
403
614
  // --- Main ---
404
615
  switch (command) {
@@ -415,6 +626,9 @@ switch (command) {
415
626
  if (args[1] === 'remove') {
416
627
  cmdAgentRemove().catch(err => { console.error('Failed:', err); process.exit(1); });
417
628
  }
629
+ else if (args[1] === 'rename') {
630
+ cmdAgentRename().catch(err => { console.error('Failed:', err); process.exit(1); });
631
+ }
418
632
  else if (args[1] === 'list') {
419
633
  cmdAgentList().catch(err => { console.error('Failed:', err); process.exit(1); });
420
634
  }
@@ -455,6 +669,14 @@ switch (command) {
455
669
  showHelp();
456
670
  }
457
671
  break;
672
+ case 'files':
673
+ if (args[1] === 'clean') {
674
+ cmdFilesClean().catch(err => { console.error('Failed:', err); process.exit(1); });
675
+ }
676
+ else {
677
+ showHelp();
678
+ }
679
+ break;
458
680
  default:
459
681
  showHelp();
460
682
  break;