kitty-hive 0.2.0 → 0.3.1
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 +101 -12
- package/README.zh.md +101 -12
- package/channel.ts +1 -1
- package/dist/db.d.ts +28 -2
- package/dist/db.js +95 -14
- package/dist/db.js.map +1 -1
- package/dist/federation-heartbeat.d.ts +7 -0
- package/dist/federation-heartbeat.js +45 -0
- package/dist/federation-heartbeat.js.map +1 -0
- package/dist/federation-http.d.ts +5 -0
- package/dist/federation-http.js +129 -37
- package/dist/federation-http.js.map +1 -1
- package/dist/index.js +445 -65
- package/dist/index.js.map +1 -1
- package/dist/mcp/federation-tools.js +27 -13
- package/dist/mcp/federation-tools.js.map +1 -1
- package/dist/mcp/task-tools.js +6 -6
- package/dist/mcp/task-tools.js.map +1 -1
- package/dist/models.d.ts +6 -0
- package/dist/models.js.map +1 -1
- package/dist/server.js +19 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/dm.js +24 -6
- package/dist/tools/dm.js.map +1 -1
- package/dist/tools/task.d.ts +6 -1
- package/dist/tools/task.js +109 -12
- package/dist/tools/task.js.map +1 -1
- package/package.json +3 -2
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, createPendingInvite, cleanupExpiredInvites } 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,81 +47,260 @@ async function cmdServe() {
|
|
|
45
47
|
setLogLevel('warn');
|
|
46
48
|
await startServer(port, dbPath);
|
|
47
49
|
}
|
|
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';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 'npx';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function readJson(path) {
|
|
63
|
+
if (!existsSync(path))
|
|
64
|
+
return {};
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
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
|
+
},
|
|
130
|
+
},
|
|
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
|
+
}
|
|
48
145
|
async function cmdInit() {
|
|
49
|
-
|
|
50
|
-
|
|
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];
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.log(`Unknown tool: "${tool}"`);
|
|
161
|
+
showInitUsage();
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
51
164
|
let port = 4123;
|
|
52
|
-
let
|
|
53
|
-
for (let i = 1; i < args.length; i++) {
|
|
165
|
+
for (let i = 2; i < args.length; i++) {
|
|
54
166
|
if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
55
|
-
port = parseInt(args[i + 1], 10);
|
|
56
|
-
explicitPort = true;
|
|
167
|
+
port = parseInt(args[i + 1], 10) || 4123;
|
|
57
168
|
i++;
|
|
58
169
|
}
|
|
59
170
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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}`);
|
|
69
184
|
}
|
|
70
|
-
catch { /* ignore */ }
|
|
71
185
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
83
222
|
}
|
|
223
|
+
function padCell(s, width) {
|
|
224
|
+
return s + ' '.repeat(Math.max(0, width - visualWidth(s)));
|
|
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'];
|
|
84
254
|
async function cmdStatus() {
|
|
85
255
|
const { port, dbPath } = parseFlags(1);
|
|
86
256
|
const url = `http://localhost:${port}/mcp`;
|
|
87
|
-
|
|
257
|
+
const nodeName = getNodeConfig().name || hostname().split('.')[0];
|
|
88
258
|
try {
|
|
89
|
-
|
|
259
|
+
await fetch(url, { method: 'GET' });
|
|
90
260
|
console.log(`🐝 Server: http://localhost:${port}/mcp (online)`);
|
|
91
261
|
}
|
|
92
262
|
catch {
|
|
93
263
|
console.log(`❌ Server: http://localhost:${port}/mcp (offline)`);
|
|
94
264
|
process.exit(1);
|
|
95
265
|
}
|
|
96
|
-
|
|
266
|
+
console.log(` Node: ${nodeName}`);
|
|
97
267
|
try {
|
|
98
268
|
const db = initDB(dbPath);
|
|
99
|
-
const agents = db.prepare(
|
|
100
|
-
const
|
|
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();
|
|
101
272
|
const tasks = db.prepare("SELECT count(*) as cnt FROM tasks WHERE status NOT IN ('completed','failed','canceled')").get();
|
|
102
|
-
|
|
103
|
-
console.log(
|
|
104
|
-
console.log(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
JOIN team_members tm ON tm.team_id = t.id
|
|
109
|
-
WHERE tm.agent_id = ? AND t.closed_at IS NULL
|
|
110
|
-
`).all(a.id);
|
|
111
|
-
const teamLabels = memberTeams.map((t) => t.nickname ? `${t.name}:${t.nickname}` : t.name).join(', ');
|
|
112
|
-
console.log(` ${a.display_name} [${a.id.slice(0, 8)}…] (${a.status}) — ${teamLabels || 'no teams'}`);
|
|
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), ' '));
|
|
113
279
|
}
|
|
114
280
|
if (teams.length > 0) {
|
|
115
|
-
console.log(`\n🏠 Teams
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
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, ' '));
|
|
119
300
|
}
|
|
120
301
|
}
|
|
121
302
|
}
|
|
122
|
-
catch
|
|
303
|
+
catch {
|
|
123
304
|
console.log(`\n⚠️ Cannot read database`);
|
|
124
305
|
}
|
|
125
306
|
}
|
|
@@ -212,14 +393,12 @@ async function cmdAgentRename() {
|
|
|
212
393
|
async function cmdAgentList() {
|
|
213
394
|
const { dbPath } = parseFlags(1);
|
|
214
395
|
const db = initDB(dbPath);
|
|
215
|
-
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();
|
|
216
397
|
if (agents.length === 0) {
|
|
217
398
|
console.log('No agents registered.');
|
|
218
399
|
return;
|
|
219
400
|
}
|
|
220
|
-
|
|
221
|
-
console.log(` ${a.display_name} (${a.status}) roles=${a.roles || 'none'} last_seen=${a.last_seen}`);
|
|
222
|
-
}
|
|
401
|
+
console.log(renderTable(AGENT_HEADERS, agentRows(db, agents)));
|
|
223
402
|
}
|
|
224
403
|
// --- Node config ---
|
|
225
404
|
function getConfigPath() {
|
|
@@ -243,7 +422,7 @@ function setNodeConfig(config) {
|
|
|
243
422
|
writeFileSync(p, JSON.stringify({ ...existing, ...config }, null, 2) + '\n');
|
|
244
423
|
}
|
|
245
424
|
function getNodeName() {
|
|
246
|
-
return getNodeConfig().name ||
|
|
425
|
+
return getNodeConfig().name || hostname().split('.')[0];
|
|
247
426
|
}
|
|
248
427
|
// --- Peer commands ---
|
|
249
428
|
async function cmdPeerAdd() {
|
|
@@ -291,6 +470,21 @@ async function cmdPeerAdd() {
|
|
|
291
470
|
console.log(` URL: ${peerUrl}`);
|
|
292
471
|
console.log(` Secret: ${secret}`);
|
|
293
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
|
+
}
|
|
294
488
|
console.log(`\n Give this secret to the peer so they can connect back.`);
|
|
295
489
|
}
|
|
296
490
|
async function cmdPeerList() {
|
|
@@ -301,11 +495,15 @@ async function cmdPeerList() {
|
|
|
301
495
|
console.log('No peers configured.');
|
|
302
496
|
return;
|
|
303
497
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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));
|
|
309
507
|
}
|
|
310
508
|
async function cmdPeerRemove() {
|
|
311
509
|
const { dbPath } = parseFlags(1);
|
|
@@ -322,6 +520,159 @@ async function cmdPeerRemove() {
|
|
|
322
520
|
console.log(`Peer "${name}" not found.`);
|
|
323
521
|
}
|
|
324
522
|
}
|
|
523
|
+
function encodeInvite(p) {
|
|
524
|
+
return 'hive://' + Buffer.from(JSON.stringify(p)).toString('base64url');
|
|
525
|
+
}
|
|
526
|
+
function decodeInvite(token) {
|
|
527
|
+
let raw = token.trim();
|
|
528
|
+
if (raw.startsWith('hive://'))
|
|
529
|
+
raw = raw.slice('hive://'.length);
|
|
530
|
+
const json = Buffer.from(raw, 'base64url').toString();
|
|
531
|
+
const p = JSON.parse(json);
|
|
532
|
+
if (p.v !== 1)
|
|
533
|
+
throw new Error('Unsupported invite version');
|
|
534
|
+
if (!p.n || !p.u || !p.s || !p.e || !p.t)
|
|
535
|
+
throw new Error('Invite missing required fields');
|
|
536
|
+
return p;
|
|
537
|
+
}
|
|
538
|
+
async function cmdPeerInvite() {
|
|
539
|
+
const { dbPath } = parseFlags(1);
|
|
540
|
+
let exposed = '';
|
|
541
|
+
let url = '';
|
|
542
|
+
let port = 4123;
|
|
543
|
+
for (let i = 2; i < args.length; i++) {
|
|
544
|
+
if ((args[i] === '--as' || args[i] === '--expose') && args[i + 1]) {
|
|
545
|
+
exposed = args[i + 1];
|
|
546
|
+
i++;
|
|
547
|
+
}
|
|
548
|
+
else if (args[i] === '--url' && args[i + 1]) {
|
|
549
|
+
url = args[i + 1];
|
|
550
|
+
i++;
|
|
551
|
+
}
|
|
552
|
+
else if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
553
|
+
port = parseInt(args[i + 1], 10) || 4123;
|
|
554
|
+
i++;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!exposed) {
|
|
558
|
+
console.log('Usage: kitty-hive peer invite --as <agent-id> [--url https://your-public-url/mcp]');
|
|
559
|
+
console.log(' --as the agent on YOUR side that the peer should be allowed to reach');
|
|
560
|
+
console.log(' --url your hive URL as the peer will see it (default http://localhost:<port>/mcp)');
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
initDB(dbPath);
|
|
564
|
+
cleanupExpiredInvites();
|
|
565
|
+
let publicUrl = url || `http://localhost:${port}/mcp`;
|
|
566
|
+
if (!/\/mcp\/?$/.test(publicUrl))
|
|
567
|
+
publicUrl = publicUrl.replace(/\/+$/, '') + '/mcp';
|
|
568
|
+
const secret = 'sk_' + generateToken().slice(0, 32);
|
|
569
|
+
const invite = createPendingInvite(secret, exposed, publicUrl);
|
|
570
|
+
const nodeName = getNodeConfig().name || hostname().split('.')[0];
|
|
571
|
+
const token = encodeInvite({ v: 1, n: nodeName, u: publicUrl, s: secret, e: exposed, t: invite.token_id });
|
|
572
|
+
console.log(`🤝 Invite created (expires in 24h)`);
|
|
573
|
+
console.log(` Your URL: ${publicUrl}`);
|
|
574
|
+
console.log(` Exposed agent: ${exposed}`);
|
|
575
|
+
console.log(` Secret: ${secret}\n`);
|
|
576
|
+
console.log(` Send this token to your peer:\n`);
|
|
577
|
+
console.log(` ${token}\n`);
|
|
578
|
+
console.log(` On the other side, run:`);
|
|
579
|
+
console.log(` kitty-hive peer accept '${token}' --as <their-agent-id>`);
|
|
580
|
+
}
|
|
581
|
+
async function cmdPeerAccept() {
|
|
582
|
+
const { dbPath } = parseFlags(1);
|
|
583
|
+
let token = '';
|
|
584
|
+
let myExposed = '';
|
|
585
|
+
let myUrl = '';
|
|
586
|
+
let port = 4123;
|
|
587
|
+
for (let i = 2; i < args.length; i++) {
|
|
588
|
+
if ((args[i] === '--as' || args[i] === '--expose') && args[i + 1]) {
|
|
589
|
+
myExposed = args[i + 1];
|
|
590
|
+
i++;
|
|
591
|
+
}
|
|
592
|
+
else if (args[i] === '--url' && args[i + 1]) {
|
|
593
|
+
myUrl = args[i + 1];
|
|
594
|
+
i++;
|
|
595
|
+
}
|
|
596
|
+
else if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
597
|
+
port = parseInt(args[i + 1], 10) || 4123;
|
|
598
|
+
i++;
|
|
599
|
+
}
|
|
600
|
+
else if (!args[i].startsWith('-') && !token)
|
|
601
|
+
token = args[i];
|
|
602
|
+
}
|
|
603
|
+
if (!token || !myExposed) {
|
|
604
|
+
console.log('Usage: kitty-hive peer accept <token> --as <your-agent-id> [--url https://your-public-url/mcp]');
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
let invite;
|
|
608
|
+
try {
|
|
609
|
+
invite = decodeInvite(token);
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
console.log(`Invalid invite: ${err.message}`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
initDB(dbPath);
|
|
616
|
+
console.log(`✓ Decoded invite from "${invite.n}"`);
|
|
617
|
+
console.log(` Their URL: ${invite.u}`);
|
|
618
|
+
console.log(` Their exposed agent: ${invite.e}\n`);
|
|
619
|
+
// Add their hive as a local peer.
|
|
620
|
+
// exposed = OUR local agent that THEY can reach (= myExposed), not their agent.
|
|
621
|
+
let peerName = invite.n;
|
|
622
|
+
if (getPeerByName(peerName)) {
|
|
623
|
+
peerName = `${invite.n}-${Date.now().toString(36).slice(-4)}`;
|
|
624
|
+
console.log(` (peer name "${invite.n}" taken; using "${peerName}")`);
|
|
625
|
+
}
|
|
626
|
+
addPeer(peerName, invite.u, invite.s, myExposed);
|
|
627
|
+
setPeerNodeName(peerName, invite.n);
|
|
628
|
+
console.log(`✓ Added ${peerName} as local peer`);
|
|
629
|
+
// Decide our URL
|
|
630
|
+
let ourUrl = myUrl || `http://localhost:${port}/mcp`;
|
|
631
|
+
if (!/\/mcp\/?$/.test(ourUrl))
|
|
632
|
+
ourUrl = ourUrl.replace(/\/+$/, '') + '/mcp';
|
|
633
|
+
const ourNode = getNodeConfig().name || hostname().split('.')[0];
|
|
634
|
+
// Handshake back: tell their hive how to reach us
|
|
635
|
+
process.stdout.write(`✓ Calling handshake on ${invite.u}…`);
|
|
636
|
+
const handshakeUrl = invite.u.replace(/\/mcp\/?$/, '/federation/handshake');
|
|
637
|
+
try {
|
|
638
|
+
const res = await fetch(handshakeUrl, {
|
|
639
|
+
method: 'POST',
|
|
640
|
+
headers: { 'Content-Type': 'application/json' },
|
|
641
|
+
body: JSON.stringify({
|
|
642
|
+
token_id: invite.t, secret: invite.s,
|
|
643
|
+
name: ourNode, url: ourUrl, exposed: myExposed,
|
|
644
|
+
}),
|
|
645
|
+
});
|
|
646
|
+
if (!res.ok) {
|
|
647
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
648
|
+
console.log(` failed: ${err.error || res.statusText}`);
|
|
649
|
+
console.log(` Local peer record was added; their side may not auto-add you.`);
|
|
650
|
+
console.log(` They can add you manually with:`);
|
|
651
|
+
console.log(` kitty-hive peer add ${ourNode} ${ourUrl} --secret ${invite.s} --expose ${myExposed}`);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const result = await res.json();
|
|
655
|
+
console.log(` ok (they added you as "${result.peer_name}")`);
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
console.log(` failed: ${err.message}`);
|
|
659
|
+
console.log(` Local peer record was added; their side may not auto-add you.`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
// Verify reachability via ping
|
|
663
|
+
process.stdout.write(`✓ Pinging ${peerName}…`);
|
|
664
|
+
const ping = await pingPeer(peerName, invite.u, invite.s, 5000);
|
|
665
|
+
if (ping.ok) {
|
|
666
|
+
setPeerStatus(peerName, 'active');
|
|
667
|
+
touchPeer(peerName);
|
|
668
|
+
console.log(` ok (node="${ping.node}")`);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
console.log(` failed: ${ping.error} (will retry on next heartbeat)`);
|
|
672
|
+
}
|
|
673
|
+
console.log(`\n🎉 Peer "${peerName}" connected. Try:`);
|
|
674
|
+
console.log(` hive-remote-agents({ peer: "${peerName}" })`);
|
|
675
|
+
}
|
|
325
676
|
async function cmdPeerExpose() {
|
|
326
677
|
const { dbPath } = parseFlags(1);
|
|
327
678
|
const peerName = args[2];
|
|
@@ -357,6 +708,18 @@ async function cmdPeerExpose() {
|
|
|
357
708
|
updatePeerExposed(peerName, current.join(','));
|
|
358
709
|
console.log(`✅ Peer "${peerName}" exposed agents: ${current.join(', ') || 'none'}`);
|
|
359
710
|
}
|
|
711
|
+
async function cmdFilesClean() {
|
|
712
|
+
let maxAgeDays = 7;
|
|
713
|
+
for (let i = 2; i < args.length; i++) {
|
|
714
|
+
if ((args[i] === '--days' || args[i] === '-d') && args[i + 1]) {
|
|
715
|
+
maxAgeDays = parseInt(args[i + 1], 10) || 7;
|
|
716
|
+
i++;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const { cleanupOldFiles } = await import('./federation-http.js');
|
|
720
|
+
const result = cleanupOldFiles(maxAgeDays);
|
|
721
|
+
console.log(`✅ Removed ${result.removed} federation file(s) older than ${maxAgeDays} day(s); kept ${result.kept}.`);
|
|
722
|
+
}
|
|
360
723
|
async function cmdConfigSet() {
|
|
361
724
|
// kitty-hive config set name marvin
|
|
362
725
|
const key = args[2];
|
|
@@ -381,24 +744,27 @@ function getDefaultAgentName() {
|
|
|
381
744
|
catch { /* ignore */ }
|
|
382
745
|
}
|
|
383
746
|
// Fallback to directory name
|
|
384
|
-
return process.cwd()
|
|
747
|
+
return basename(process.cwd()) || 'agent';
|
|
385
748
|
}
|
|
386
749
|
function showHelp() {
|
|
387
750
|
console.log(`🐝 kitty-hive — multi-agent collaboration server
|
|
388
751
|
|
|
389
752
|
Usage:
|
|
390
753
|
kitty-hive serve [--port 4123] [--db path] [-v|-q] Start the server
|
|
391
|
-
kitty-hive init [--port 4123]
|
|
754
|
+
kitty-hive init <tool> [--port 4123] Write MCP config (claude|cursor|vscode|antigravity|all)
|
|
392
755
|
kitty-hive status [--port 4123] Server & agent status
|
|
393
756
|
kitty-hive agent list List agents
|
|
394
757
|
kitty-hive agent rename <old> <new> Rename an agent
|
|
395
758
|
kitty-hive agent remove <name> Remove an agent
|
|
396
|
-
kitty-hive peer
|
|
759
|
+
kitty-hive peer invite --as <agent> [--url url] Create invite token (recommended)
|
|
760
|
+
kitty-hive peer accept <token> --as <agent> [--url url] Accept an invite token
|
|
761
|
+
kitty-hive peer add <name> <url> [--expose a,b] [--secret s] Add a peer (manual)
|
|
397
762
|
kitty-hive peer list List peers
|
|
398
763
|
kitty-hive peer remove <name> Remove a peer
|
|
399
764
|
kitty-hive peer expose <name> --add/--remove <agent> Manage exposed agents
|
|
400
765
|
kitty-hive config set <key> <value> Set config (e.g. name)
|
|
401
|
-
kitty-hive db clear [--db path] Clear the database
|
|
766
|
+
kitty-hive db clear [--db path] Clear the database
|
|
767
|
+
kitty-hive files clean [--days 7] Remove old federation transfer files`);
|
|
402
768
|
}
|
|
403
769
|
// --- Main ---
|
|
404
770
|
switch (command) {
|
|
@@ -438,6 +804,12 @@ switch (command) {
|
|
|
438
804
|
else if (args[1] === 'expose') {
|
|
439
805
|
cmdPeerExpose().catch(err => { console.error('Failed:', err); process.exit(1); });
|
|
440
806
|
}
|
|
807
|
+
else if (args[1] === 'invite') {
|
|
808
|
+
cmdPeerInvite().catch(err => { console.error('Failed:', err); process.exit(1); });
|
|
809
|
+
}
|
|
810
|
+
else if (args[1] === 'accept') {
|
|
811
|
+
cmdPeerAccept().catch(err => { console.error('Failed:', err); process.exit(1); });
|
|
812
|
+
}
|
|
441
813
|
else {
|
|
442
814
|
showHelp();
|
|
443
815
|
}
|
|
@@ -458,6 +830,14 @@ switch (command) {
|
|
|
458
830
|
showHelp();
|
|
459
831
|
}
|
|
460
832
|
break;
|
|
833
|
+
case 'files':
|
|
834
|
+
if (args[1] === 'clean') {
|
|
835
|
+
cmdFilesClean().catch(err => { console.error('Failed:', err); process.exit(1); });
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
showHelp();
|
|
839
|
+
}
|
|
840
|
+
break;
|
|
461
841
|
default:
|
|
462
842
|
showHelp();
|
|
463
843
|
break;
|