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.
- package/README.md +121 -126
- package/README.zh.md +153 -105
- package/channel.ts +194 -431
- package/dist/auth.d.ts +10 -6
- package/dist/auth.js +32 -17
- package/dist/auth.js.map +1 -1
- package/dist/db.d.ts +47 -17
- package/dist/db.js +268 -123
- 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 +7 -0
- package/dist/federation-http.js +287 -0
- package/dist/federation-http.js.map +1 -0
- package/dist/index.js +336 -114
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +3 -0
- package/dist/log.js +10 -0
- package/dist/log.js.map +1 -0
- package/dist/mcp/agent-tools.d.ts +2 -0
- package/dist/mcp/agent-tools.js +66 -0
- package/dist/mcp/agent-tools.js.map +1 -0
- package/dist/mcp/dm-tools.d.ts +2 -0
- package/dist/mcp/dm-tools.js +54 -0
- package/dist/mcp/dm-tools.js.map +1 -0
- package/dist/mcp/federation-tools.d.ts +2 -0
- package/dist/mcp/federation-tools.js +44 -0
- package/dist/mcp/federation-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +62 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/task-tools.d.ts +2 -0
- package/dist/mcp/task-tools.js +128 -0
- package/dist/mcp/task-tools.js.map +1 -0
- package/dist/mcp/team-tools.d.ts +2 -0
- package/dist/mcp/team-tools.js +95 -0
- package/dist/mcp/team-tools.js.map +1 -0
- package/dist/models.d.ts +28 -10
- package/dist/models.js +1 -3
- package/dist/models.js.map +1 -1
- package/dist/server.d.ts +2 -3
- package/dist/server.js +56 -761
- package/dist/server.js.map +1 -1
- package/dist/sessions.d.ts +15 -0
- package/dist/sessions.js +107 -0
- package/dist/sessions.js.map +1 -0
- package/dist/tools/dm.d.ts +3 -2
- package/dist/tools/dm.js +35 -32
- package/dist/tools/dm.js.map +1 -1
- package/dist/tools/start.d.ts +3 -3
- package/dist/tools/start.js +18 -15
- package/dist/tools/start.js.map +1 -1
- package/dist/tools/task.d.ts +7 -2
- package/dist/tools/task.js +113 -15
- package/dist/tools/task.js.map +1 -1
- package/dist/tools/team.d.ts +62 -13
- package/dist/tools/team.js +102 -30
- package/dist/tools/team.js.map +1 -1
- package/package.json +4 -3
- package/dist/tools/room.d.ts +0 -39
- package/dist/tools/room.js +0 -35
- 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
port = parseInt(p, 10) || 4123;
|
|
58
|
+
catch {
|
|
59
|
+
return 'npx';
|
|
78
60
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
61
|
+
}
|
|
62
|
+
function readJson(path) {
|
|
63
|
+
if (!existsSync(path))
|
|
64
|
+
return {};
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
83
67
|
}
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
delete existing.mcpServers['hive-channel'];
|
|
160
|
+
console.log(`Unknown tool: "${tool}"`);
|
|
161
|
+
showInitUsage();
|
|
162
|
+
process.exit(1);
|
|
111
163
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
257
|
+
const nodeName = getNodeConfig().name || hostname().split('.')[0];
|
|
125
258
|
try {
|
|
126
|
-
|
|
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
|
-
|
|
266
|
+
console.log(` Node: ${nodeName}`);
|
|
134
267
|
try {
|
|
135
268
|
const db = initDB(dbPath);
|
|
136
|
-
const agents = db.prepare(
|
|
137
|
-
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();
|
|
138
272
|
const tasks = db.prepare("SELECT count(*) as cnt FROM tasks WHERE status NOT IN ('completed','failed','canceled')").get();
|
|
139
|
-
|
|
140
|
-
console.log(
|
|
141
|
-
console.log(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 (
|
|
155
|
-
console.log(`\nπ
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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
|
|
191
|
-
if (!
|
|
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
|
|
197
|
-
if (
|
|
198
|
-
console.log(`Agent "${
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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()
|
|
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
|
|
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;
|