kitty-hive 0.5.2 → 0.5.4
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 +66 -48
- package/README.zh.md +67 -49
- package/channel.ts +1 -1
- package/dist/index.js +477 -195
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts +36 -0
- package/dist/interactive.js +94 -0
- package/dist/interactive.js.map +1 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -4,11 +4,11 @@ import { initDB, addPeer, listPeers, removePeer, updatePeerExposed, getPeerByNam
|
|
|
4
4
|
import { pingPeer } from './federation-heartbeat.js';
|
|
5
5
|
import { TunnelManager, findCloudflared } from './tunnel.js';
|
|
6
6
|
import { generateToken } from './utils.js';
|
|
7
|
+
import { askText, askSelect, askConfirm, pickLocalAgent, pickLocalAgents, pickPeer, isLocalAgent, isInteractive, } from './interactive.js';
|
|
7
8
|
import { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
8
9
|
import { join, dirname, basename, delimiter } from 'node:path';
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { homedir, hostname } from 'node:os';
|
|
11
|
-
import { createInterface } from 'node:readline';
|
|
12
12
|
import { execSync } from 'node:child_process';
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
14
|
const __dirname = dirname(__filename);
|
|
@@ -29,16 +29,6 @@ function parseFlags(startIdx) {
|
|
|
29
29
|
}
|
|
30
30
|
return { port, dbPath };
|
|
31
31
|
}
|
|
32
|
-
async function ask(question, defaultValue) {
|
|
33
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
-
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
35
|
-
return new Promise(resolve => {
|
|
36
|
-
rl.question(`${question}${suffix}: `, answer => {
|
|
37
|
-
rl.close();
|
|
38
|
-
resolve(answer.trim() || defaultValue || '');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
32
|
// --- Commands ---
|
|
43
33
|
async function cmdServe() {
|
|
44
34
|
const { port, dbPath } = parseFlags(1);
|
|
@@ -144,12 +134,31 @@ function showInitUsage() {
|
|
|
144
134
|
console.log(' all run all of the above');
|
|
145
135
|
}
|
|
146
136
|
async function cmdInit() {
|
|
147
|
-
const tool = args[1];
|
|
148
|
-
if (!tool || tool.startsWith('-')) {
|
|
149
|
-
showInitUsage();
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
137
|
const known = ALL_INIT_TARGETS;
|
|
138
|
+
let tool = args[1] && !args[1].startsWith('-') ? args[1] : '';
|
|
139
|
+
let port = 4123;
|
|
140
|
+
for (let i = 2; i < args.length; i++) {
|
|
141
|
+
if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
142
|
+
port = parseInt(args[i + 1], 10) || 4123;
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!tool) {
|
|
147
|
+
if (!isInteractive()) {
|
|
148
|
+
showInitUsage();
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
tool = await askSelect({
|
|
152
|
+
message: 'Which tool should we configure?',
|
|
153
|
+
options: [
|
|
154
|
+
{ value: 'claude', label: 'Claude Code', hint: '.mcp.json (prefer the plugin if installed)' },
|
|
155
|
+
{ value: 'cursor', label: 'Cursor', hint: '.cursor/mcp.json' },
|
|
156
|
+
{ value: 'vscode', label: 'VS Code Copilot', hint: '.vscode/mcp.json' },
|
|
157
|
+
{ value: 'antigravity', label: 'Antigravity', hint: 'snippet — paste via MCP Store UI' },
|
|
158
|
+
{ value: 'all', label: 'All of the above' },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
153
162
|
let targets;
|
|
154
163
|
if (tool === 'all') {
|
|
155
164
|
targets = [...ALL_INIT_TARGETS];
|
|
@@ -162,13 +171,6 @@ async function cmdInit() {
|
|
|
162
171
|
showInitUsage();
|
|
163
172
|
process.exit(1);
|
|
164
173
|
}
|
|
165
|
-
let port = 4123;
|
|
166
|
-
for (let i = 2; i < args.length; i++) {
|
|
167
|
-
if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
168
|
-
port = parseInt(args[i + 1], 10) || 4123;
|
|
169
|
-
i++;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
174
|
console.log(`🐝 Configuring hive → http://localhost:${port}/mcp\n`);
|
|
173
175
|
for (const t of targets) {
|
|
174
176
|
if (t === 'antigravity') {
|
|
@@ -315,8 +317,11 @@ async function cmdDbClear() {
|
|
|
315
317
|
console.log('Database does not exist.');
|
|
316
318
|
process.exit(0);
|
|
317
319
|
}
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
+
const ok = await askConfirm({
|
|
321
|
+
message: `Delete ${resolvedPath}?`,
|
|
322
|
+
initialValue: false,
|
|
323
|
+
});
|
|
324
|
+
if (!ok) {
|
|
320
325
|
console.log('Cancelled.');
|
|
321
326
|
process.exit(0);
|
|
322
327
|
}
|
|
@@ -330,26 +335,36 @@ async function cmdDbClear() {
|
|
|
330
335
|
async function cmdAgentRemove() {
|
|
331
336
|
const { dbPath } = parseFlags(1);
|
|
332
337
|
const target = args[2];
|
|
333
|
-
if (!target) {
|
|
334
|
-
console.log('Usage: kitty-hive agent remove <name-or-id>');
|
|
335
|
-
process.exit(1);
|
|
336
|
-
}
|
|
337
338
|
const db = initDB(dbPath);
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
let agent;
|
|
340
|
+
if (!target) {
|
|
341
|
+
if (!isInteractive()) {
|
|
342
|
+
console.log('Usage: kitty-hive agent remove <name-or-id>');
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
const id = await pickLocalAgent(db, 'Remove which agent?');
|
|
346
|
+
agent = db.prepare('SELECT id, display_name FROM agents WHERE id = ?').get(id);
|
|
342
347
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
console.log(`
|
|
347
|
-
|
|
348
|
+
else {
|
|
349
|
+
const matches = db.prepare('SELECT id, display_name FROM agents WHERE id = ? OR display_name = ?').all(target, target);
|
|
350
|
+
if (matches.length === 0) {
|
|
351
|
+
console.log(`Agent "${target}" not found.`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
if (matches.length > 1) {
|
|
355
|
+
console.log(`"${target}" matches ${matches.length} agents. Use id to disambiguate.`);
|
|
356
|
+
for (const m of matches)
|
|
357
|
+
console.log(` ${m.id} ${m.display_name}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
agent = matches[0];
|
|
348
361
|
}
|
|
349
|
-
const agent = matches[0];
|
|
350
362
|
const name = agent.display_name;
|
|
351
|
-
const
|
|
352
|
-
|
|
363
|
+
const ok = await askConfirm({
|
|
364
|
+
message: `Remove agent "${name}" (${agent.id}) and all related data?`,
|
|
365
|
+
initialValue: false,
|
|
366
|
+
});
|
|
367
|
+
if (!ok) {
|
|
353
368
|
console.log('Cancelled.');
|
|
354
369
|
process.exit(0);
|
|
355
370
|
}
|
|
@@ -376,23 +391,44 @@ async function cmdAgentRemove() {
|
|
|
376
391
|
async function cmdAgentRename() {
|
|
377
392
|
const { dbPath } = parseFlags(1);
|
|
378
393
|
const oldName = args[2];
|
|
379
|
-
|
|
380
|
-
if (!oldName || !newName) {
|
|
381
|
-
console.log('Usage: kitty-hive agent rename <old-name> <new-name>');
|
|
382
|
-
process.exit(1);
|
|
383
|
-
}
|
|
394
|
+
let newName = args[3];
|
|
384
395
|
const db = initDB(dbPath);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
396
|
+
let agentId;
|
|
397
|
+
let oldDisplay;
|
|
398
|
+
if (!oldName) {
|
|
399
|
+
if (!isInteractive()) {
|
|
400
|
+
console.log('Usage: kitty-hive agent rename <old-name-or-id> <new-name>');
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
agentId = await pickLocalAgent(db, 'Rename which agent?');
|
|
404
|
+
oldDisplay = db.prepare('SELECT display_name FROM agents WHERE id = ?').get(agentId).display_name;
|
|
389
405
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
406
|
+
else {
|
|
407
|
+
const matches = db.prepare('SELECT id, display_name FROM agents WHERE display_name = ? OR id = ?').all(oldName, oldName);
|
|
408
|
+
if (matches.length === 0) {
|
|
409
|
+
console.log(`Agent "${oldName}" not found.`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
if (matches.length > 1) {
|
|
413
|
+
console.log(`"${oldName}" matches ${matches.length} agents. Use agent id to disambiguate.`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
agentId = matches[0].id;
|
|
417
|
+
oldDisplay = matches[0].display_name;
|
|
418
|
+
}
|
|
419
|
+
if (!newName) {
|
|
420
|
+
if (!isInteractive()) {
|
|
421
|
+
console.log('Usage: kitty-hive agent rename <old-name-or-id> <new-name>');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
newName = await askText({
|
|
425
|
+
message: `New name for "${oldDisplay}"`,
|
|
426
|
+
placeholder: oldDisplay,
|
|
427
|
+
validate: (v) => ((v ?? '').trim().length === 0 ? 'Name cannot be empty' : undefined),
|
|
428
|
+
});
|
|
393
429
|
}
|
|
394
|
-
db.prepare('UPDATE agents SET display_name = ? WHERE id = ?').run(newName,
|
|
395
|
-
console.log(`✅ Renamed "${
|
|
430
|
+
db.prepare('UPDATE agents SET display_name = ? WHERE id = ?').run(newName, agentId);
|
|
431
|
+
console.log(`✅ Renamed "${oldDisplay}" → "${newName}".`);
|
|
396
432
|
}
|
|
397
433
|
async function cmdAgentList() {
|
|
398
434
|
const { dbPath } = parseFlags(1);
|
|
@@ -457,15 +493,54 @@ async function cmdPeerAdd() {
|
|
|
457
493
|
positional++;
|
|
458
494
|
}
|
|
459
495
|
}
|
|
496
|
+
const db = initDB(dbPath);
|
|
460
497
|
if (!peerName || !peerUrl) {
|
|
461
|
-
|
|
462
|
-
|
|
498
|
+
if (!isInteractive()) {
|
|
499
|
+
console.log('Usage: kitty-hive peer add <name> <url> [--expose agent1,agent2] [--secret s]');
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
if (!peerName) {
|
|
503
|
+
peerName = await askText({
|
|
504
|
+
message: 'Peer name (a label for this peer on your side)',
|
|
505
|
+
validate: (v) => {
|
|
506
|
+
const t = (v ?? '').trim();
|
|
507
|
+
if (!t)
|
|
508
|
+
return 'Name cannot be empty';
|
|
509
|
+
if (getPeerByName(t))
|
|
510
|
+
return `Peer "${t}" already exists`;
|
|
511
|
+
return undefined;
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (!peerUrl) {
|
|
516
|
+
peerUrl = await askText({
|
|
517
|
+
message: 'Peer URL (e.g. https://xxx.trycloudflare.com/mcp)',
|
|
518
|
+
validate: (v) => (!/^https?:\/\//.test((v ?? '').trim()) ? 'URL must start with http:// or https://' : undefined),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
463
521
|
}
|
|
464
|
-
initDB(dbPath);
|
|
465
522
|
if (getPeerByName(peerName)) {
|
|
466
523
|
console.log(`Peer "${peerName}" already exists. Remove it first.`);
|
|
467
524
|
process.exit(1);
|
|
468
525
|
}
|
|
526
|
+
// Interactive: pick exposed agents from local list (skips the bug where you
|
|
527
|
+
// could pass non-existent IDs).
|
|
528
|
+
if (!exposed && isInteractive()) {
|
|
529
|
+
const picked = await pickLocalAgents(db, 'Which local agents should this peer be allowed to reach?');
|
|
530
|
+
exposed = picked.join(',');
|
|
531
|
+
}
|
|
532
|
+
if (!secret && isInteractive()) {
|
|
533
|
+
const provideSecret = await askConfirm({
|
|
534
|
+
message: 'Use an existing secret? (No = generate one for you)',
|
|
535
|
+
initialValue: false,
|
|
536
|
+
});
|
|
537
|
+
if (provideSecret) {
|
|
538
|
+
secret = await askText({
|
|
539
|
+
message: 'Paste shared secret',
|
|
540
|
+
validate: (v) => ((v ?? '').trim().length < 8 ? 'Secret looks too short' : undefined),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
469
544
|
if (!secret) {
|
|
470
545
|
secret = 'sk_' + generateToken().slice(0, 32);
|
|
471
546
|
}
|
|
@@ -473,7 +548,7 @@ async function cmdPeerAdd() {
|
|
|
473
548
|
console.log(`🤝 Peer added: ${peerName}`);
|
|
474
549
|
console.log(` URL: ${peerUrl}`);
|
|
475
550
|
console.log(` Secret: ${secret}`);
|
|
476
|
-
console.log(` Exposed agents: ${exposed || 'none (use
|
|
551
|
+
console.log(` Exposed agents: ${exposed || 'none (use `peer expose` later)'}`);
|
|
477
552
|
// Verify reachability via /federation/ping
|
|
478
553
|
process.stdout.write(` Pinging…`);
|
|
479
554
|
const result = await pingPeer(peerName, peerUrl, secret, 5000);
|
|
@@ -511,17 +586,29 @@ async function cmdPeerList() {
|
|
|
511
586
|
}
|
|
512
587
|
async function cmdPeerRemove() {
|
|
513
588
|
const { dbPath } = parseFlags(1);
|
|
514
|
-
|
|
589
|
+
let name = args[2];
|
|
590
|
+
initDB(dbPath);
|
|
515
591
|
if (!name) {
|
|
516
|
-
|
|
517
|
-
|
|
592
|
+
if (!isInteractive()) {
|
|
593
|
+
console.log('Usage: kitty-hive peer remove <name>');
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
name = await pickPeer('Remove which peer?');
|
|
597
|
+
const ok = await askConfirm({
|
|
598
|
+
message: `Remove peer "${name}"?`,
|
|
599
|
+
initialValue: false,
|
|
600
|
+
});
|
|
601
|
+
if (!ok) {
|
|
602
|
+
console.log('Cancelled.');
|
|
603
|
+
process.exit(0);
|
|
604
|
+
}
|
|
518
605
|
}
|
|
519
|
-
initDB(dbPath);
|
|
520
606
|
if (removePeer(name)) {
|
|
521
607
|
console.log(`✅ Peer "${name}" removed.`);
|
|
522
608
|
}
|
|
523
609
|
else {
|
|
524
610
|
console.log(`Peer "${name}" not found.`);
|
|
611
|
+
process.exit(1);
|
|
525
612
|
}
|
|
526
613
|
}
|
|
527
614
|
// --- Tunnel ---
|
|
@@ -557,12 +644,16 @@ async function ensureNodeName() {
|
|
|
557
644
|
if (explicit)
|
|
558
645
|
return explicit;
|
|
559
646
|
const defaultName = hostname().split('.')[0];
|
|
560
|
-
if (!
|
|
647
|
+
if (!isInteractive()) {
|
|
561
648
|
// Non-interactive (e.g. systemd) — silently use hostname
|
|
562
649
|
return defaultName;
|
|
563
650
|
}
|
|
564
651
|
console.log(`\n📛 No node name set. Peers will see you under this name.`);
|
|
565
|
-
const answer = (await
|
|
652
|
+
const answer = (await askText({
|
|
653
|
+
message: 'Node name',
|
|
654
|
+
placeholder: defaultName,
|
|
655
|
+
initialValue: defaultName,
|
|
656
|
+
})).trim() || defaultName;
|
|
566
657
|
setNodeConfig({ name: answer });
|
|
567
658
|
console.log(` → saved (kitty-hive config set name ${answer})\n`);
|
|
568
659
|
return answer;
|
|
@@ -693,16 +784,23 @@ async function cmdPeerInvite() {
|
|
|
693
784
|
i++;
|
|
694
785
|
}
|
|
695
786
|
}
|
|
787
|
+
const db = initDB(dbPath);
|
|
788
|
+
cleanupExpiredInvites();
|
|
696
789
|
if (!exposed) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
790
|
+
if (!isInteractive()) {
|
|
791
|
+
console.log('Usage: kitty-hive peer invite --expose <my-agent-id>');
|
|
792
|
+
console.log(' --expose YOUR local agent that the peer should be allowed to reach');
|
|
793
|
+
console.log('');
|
|
794
|
+
console.log(' Cross-machine? Run `kitty-hive tunnel start` first; the URL will be picked up');
|
|
795
|
+
console.log(' automatically. (Advanced: --url <https://.../mcp> overrides.)');
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
exposed = await pickLocalAgent(db, 'Which local agent should the invitee be allowed to reach?');
|
|
799
|
+
}
|
|
800
|
+
else if (!isLocalAgent(db, exposed)) {
|
|
801
|
+
console.log(`❌ Agent "${exposed}" is not a local agent (must be one registered on this hive).`);
|
|
702
802
|
process.exit(1);
|
|
703
803
|
}
|
|
704
|
-
initDB(dbPath);
|
|
705
|
-
cleanupExpiredInvites();
|
|
706
804
|
// Resolve URL: explicit --url > tunnel URL stored by `kitty-hive tunnel start`.
|
|
707
805
|
// Refuse if neither — invites are only meaningful across machines.
|
|
708
806
|
const stored = getNodeState('public_url');
|
|
@@ -752,14 +850,17 @@ async function cmdPeerAccept() {
|
|
|
752
850
|
else if (!args[i].startsWith('-') && !token)
|
|
753
851
|
token = args[i];
|
|
754
852
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
853
|
+
const db = initDB(dbPath);
|
|
854
|
+
if (!token) {
|
|
855
|
+
if (!isInteractive()) {
|
|
856
|
+
console.log('Usage: kitty-hive peer accept <token> --expose <my-agent-id>');
|
|
857
|
+
console.log(' (Token must be provided as a positional argument or interactively.)');
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
token = await askText({
|
|
861
|
+
message: 'Paste invite token (starts with hive://)',
|
|
862
|
+
validate: (v) => (!(v ?? '').trim().startsWith('hive://') ? 'Token must start with hive://' : undefined),
|
|
863
|
+
});
|
|
763
864
|
}
|
|
764
865
|
let invite;
|
|
765
866
|
try {
|
|
@@ -769,7 +870,18 @@ async function cmdPeerAccept() {
|
|
|
769
870
|
console.log(`Invalid invite: ${err.message}`);
|
|
770
871
|
process.exit(1);
|
|
771
872
|
}
|
|
772
|
-
|
|
873
|
+
if (!myExposed) {
|
|
874
|
+
if (!isInteractive()) {
|
|
875
|
+
console.log('Usage: kitty-hive peer accept <token> --expose <my-agent-id>');
|
|
876
|
+
console.log(' --expose YOUR local agent that the inviter should be allowed to reach');
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
myExposed = await pickLocalAgent(db, 'Which local agent should the inviter be allowed to reach?');
|
|
880
|
+
}
|
|
881
|
+
else if (!isLocalAgent(db, myExposed)) {
|
|
882
|
+
console.log(`❌ Agent "${myExposed}" is not a local agent on this hive.`);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
773
885
|
console.log(`✓ Decoded invite from "${invite.n}"`);
|
|
774
886
|
console.log(` Their URL: ${invite.u}`);
|
|
775
887
|
console.log(` Their exposed agent: ${invite.e}\n`);
|
|
@@ -839,20 +951,34 @@ async function cmdPeerAccept() {
|
|
|
839
951
|
}
|
|
840
952
|
async function cmdPeerSetUrl() {
|
|
841
953
|
const { dbPath } = parseFlags(1);
|
|
842
|
-
|
|
954
|
+
let peerName = args[2];
|
|
843
955
|
let newUrl = args[3];
|
|
844
|
-
if (!peerName || !newUrl) {
|
|
845
|
-
console.log('Usage: kitty-hive peer set-url <peer-name> <new-url>');
|
|
846
|
-
console.log(' Use this when a peer\'s tunnel URL changes and the auto-sync didn\'t reach you');
|
|
847
|
-
console.log(' (e.g. you were both offline at the same time).');
|
|
848
|
-
process.exit(1);
|
|
849
|
-
}
|
|
850
956
|
initDB(dbPath);
|
|
957
|
+
if (!peerName) {
|
|
958
|
+
if (!isInteractive()) {
|
|
959
|
+
console.log('Usage: kitty-hive peer set-url <peer-name> <new-url>');
|
|
960
|
+
console.log(' Use this when a peer\'s tunnel URL changes and the auto-sync didn\'t reach you');
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
peerName = await pickPeer('Which peer\'s URL needs updating?');
|
|
964
|
+
}
|
|
851
965
|
const peer = getPeerByName(peerName);
|
|
852
966
|
if (!peer) {
|
|
853
967
|
console.log(`Peer "${peerName}" not found.`);
|
|
854
968
|
process.exit(1);
|
|
855
969
|
}
|
|
970
|
+
if (!newUrl) {
|
|
971
|
+
if (!isInteractive()) {
|
|
972
|
+
console.log('Usage: kitty-hive peer set-url <peer-name> <new-url>');
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
newUrl = await askText({
|
|
976
|
+
message: `New URL for "${peerName}"`,
|
|
977
|
+
placeholder: peer.url,
|
|
978
|
+
initialValue: peer.url,
|
|
979
|
+
validate: (v) => (!/^https?:\/\//i.test((v ?? '').trim()) ? 'URL must start with http:// or https://' : undefined),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
856
982
|
if (!/^https?:\/\//i.test(newUrl)) {
|
|
857
983
|
console.log(`URL must start with http:// or https:// — got "${newUrl}"`);
|
|
858
984
|
process.exit(1);
|
|
@@ -880,38 +1006,83 @@ async function cmdPeerSetUrl() {
|
|
|
880
1006
|
}
|
|
881
1007
|
async function cmdPeerExpose() {
|
|
882
1008
|
const { dbPath } = parseFlags(1);
|
|
883
|
-
|
|
1009
|
+
// Usage:
|
|
1010
|
+
// peer expose <name> TTY → multiselect; non-TTY → show current
|
|
1011
|
+
// peer expose <name> id1,id2,id3 Replace exposure list (script-friendly)
|
|
1012
|
+
// peer expose <name> --clear Empty the exposure list
|
|
1013
|
+
let peerName = '';
|
|
1014
|
+
let replacement = null;
|
|
1015
|
+
let clear = false;
|
|
1016
|
+
let positional = 0;
|
|
1017
|
+
for (let i = 2; i < args.length; i++) {
|
|
1018
|
+
if (args[i] === '--clear') {
|
|
1019
|
+
clear = true;
|
|
1020
|
+
}
|
|
1021
|
+
else if (args[i] === '--port' || args[i] === '-p' || args[i] === '--db') {
|
|
1022
|
+
i++;
|
|
1023
|
+
}
|
|
1024
|
+
else if (!args[i].startsWith('-')) {
|
|
1025
|
+
if (positional === 0)
|
|
1026
|
+
peerName = args[i];
|
|
1027
|
+
else if (positional === 1)
|
|
1028
|
+
replacement = args[i];
|
|
1029
|
+
positional++;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const db = initDB(dbPath);
|
|
884
1033
|
if (!peerName) {
|
|
885
|
-
|
|
886
|
-
|
|
1034
|
+
if (!isInteractive()) {
|
|
1035
|
+
console.log('Usage: kitty-hive peer expose <peer> [<id1,id2,...>]');
|
|
1036
|
+
console.log(' kitty-hive peer expose <peer> --clear');
|
|
1037
|
+
console.log(' No second argument: show current exposure (or interactive picker in a TTY).');
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
peerName = await pickPeer('Manage exposed agents for which peer?');
|
|
887
1041
|
}
|
|
888
|
-
initDB(dbPath);
|
|
889
1042
|
const peer = getPeerByName(peerName);
|
|
890
1043
|
if (!peer) {
|
|
891
1044
|
console.log(`Peer "${peerName}" not found.`);
|
|
892
1045
|
process.exit(1);
|
|
893
1046
|
}
|
|
894
1047
|
const current = peer.exposed ? peer.exposed.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
current.push(a);
|
|
901
|
-
i++;
|
|
1048
|
+
// --- View path: no second arg, no --clear, non-TTY ---
|
|
1049
|
+
if (replacement === null && !clear && !isInteractive()) {
|
|
1050
|
+
if (current.length === 0) {
|
|
1051
|
+
console.log(`Peer "${peerName}" exposes no local agents.`);
|
|
1052
|
+
return;
|
|
902
1053
|
}
|
|
903
|
-
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1054
|
+
const named = current.map(id => {
|
|
1055
|
+
const row = db.prepare('SELECT display_name FROM agents WHERE id = ?').get(id);
|
|
1056
|
+
return row?.display_name ? ` ${row.display_name} (${id})` : ` ${id} (unknown — agent may have been removed)`;
|
|
1057
|
+
});
|
|
1058
|
+
console.log(`Peer "${peerName}" exposes ${current.length} local agent(s):`);
|
|
1059
|
+
console.log(named.join('\n'));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
// --- Edit paths ---
|
|
1063
|
+
let next;
|
|
1064
|
+
if (clear) {
|
|
1065
|
+
next = [];
|
|
1066
|
+
}
|
|
1067
|
+
else if (replacement !== null) {
|
|
1068
|
+
// Positional replacement — replace the whole list.
|
|
1069
|
+
const parsed = replacement.split(',').map(s => s.trim()).filter(Boolean);
|
|
1070
|
+
next = [];
|
|
1071
|
+
for (const a of parsed) {
|
|
1072
|
+
if (!isLocalAgent(db, a)) {
|
|
1073
|
+
console.log(`❌ "${a}" is not a local agent on this hive — skipping. (Remote placeholder agents can't be exposed.)`);
|
|
1074
|
+
continue;
|
|
909
1075
|
}
|
|
910
|
-
|
|
1076
|
+
if (!next.includes(a))
|
|
1077
|
+
next.push(a);
|
|
911
1078
|
}
|
|
912
1079
|
}
|
|
913
|
-
|
|
914
|
-
|
|
1080
|
+
else {
|
|
1081
|
+
// No args + TTY → interactive multiselect (current pre-checked).
|
|
1082
|
+
next = await pickLocalAgents(db, `Local agents to expose to "${peerName}" (Space to toggle, Enter to confirm)`, current);
|
|
1083
|
+
}
|
|
1084
|
+
updatePeerExposed(peerName, next.join(','));
|
|
1085
|
+
console.log(`✅ Peer "${peerName}" exposed agents: ${next.join(', ') || 'none'}`);
|
|
915
1086
|
}
|
|
916
1087
|
async function cmdFilesClean() {
|
|
917
1088
|
let maxAgeDays = 7;
|
|
@@ -925,14 +1096,34 @@ async function cmdFilesClean() {
|
|
|
925
1096
|
const result = cleanupOldFiles(maxAgeDays);
|
|
926
1097
|
console.log(`✅ Removed ${result.removed} federation file(s) older than ${maxAgeDays} day(s); kept ${result.kept}.`);
|
|
927
1098
|
}
|
|
1099
|
+
const CONFIG_KEYS = ['name'];
|
|
928
1100
|
async function cmdConfigSet() {
|
|
929
1101
|
// kitty-hive config set name marvin
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
if (!key
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1102
|
+
let key = args[2];
|
|
1103
|
+
let value = args[3];
|
|
1104
|
+
if (!key) {
|
|
1105
|
+
if (!isInteractive()) {
|
|
1106
|
+
console.log('Usage: kitty-hive config set <key> <value>');
|
|
1107
|
+
console.log(` Known keys: ${CONFIG_KEYS.join(', ')}`);
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
key = await askSelect({
|
|
1111
|
+
message: 'Which config key?',
|
|
1112
|
+
options: CONFIG_KEYS.map(k => ({ value: k, label: k })),
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (!value) {
|
|
1116
|
+
if (!isInteractive()) {
|
|
1117
|
+
console.log('Usage: kitty-hive config set <key> <value>');
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
const current = getNodeConfig()[key] || '';
|
|
1121
|
+
value = await askText({
|
|
1122
|
+
message: `Value for "${key}"`,
|
|
1123
|
+
placeholder: current || (key === 'name' ? hostname().split('.')[0] : ''),
|
|
1124
|
+
initialValue: current,
|
|
1125
|
+
validate: (v) => ((v ?? '').trim().length === 0 ? 'Value cannot be empty' : undefined),
|
|
1126
|
+
});
|
|
936
1127
|
}
|
|
937
1128
|
setNodeConfig({ [key]: value });
|
|
938
1129
|
console.log(`✅ ${key} = ${value}`);
|
|
@@ -951,117 +1142,208 @@ function getDefaultAgentName() {
|
|
|
951
1142
|
// Fallback to directory name
|
|
952
1143
|
return basename(process.cwd()) || 'agent';
|
|
953
1144
|
}
|
|
954
|
-
|
|
1145
|
+
// --- Help (per-group + root) ---
|
|
1146
|
+
function showRootHelp() {
|
|
955
1147
|
console.log(`🐝 kitty-hive — multi-agent collaboration server
|
|
956
1148
|
|
|
957
1149
|
Usage:
|
|
958
|
-
kitty-hive
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1150
|
+
kitty-hive <command> [args]
|
|
1151
|
+
|
|
1152
|
+
Top-level commands:
|
|
1153
|
+
serve [--port 4123] [--db path] [-v|-q] Start the MCP server
|
|
1154
|
+
init [tool] [--port 4123] Write MCP config (claude|cursor|vscode|antigravity|all)
|
|
1155
|
+
status [--port 4123] Server & agent status
|
|
1156
|
+
|
|
1157
|
+
Command groups:
|
|
1158
|
+
agent Manage local agents (list, rename, remove)
|
|
1159
|
+
peer Manage federation peers (invite, accept, add, list, expose, set-url, remove)
|
|
1160
|
+
tunnel Cloudflare tunnel control (start, status)
|
|
1161
|
+
config Node config (set)
|
|
1162
|
+
files Federation files (clean)
|
|
1163
|
+
db Database maintenance (clear)
|
|
1164
|
+
|
|
1165
|
+
Run \`kitty-hive <group>\` to see that group's subcommands.
|
|
1166
|
+
Most commands prompt for missing arguments interactively.`);
|
|
1167
|
+
}
|
|
1168
|
+
function showAgentHelp() {
|
|
1169
|
+
console.log(`🐝 kitty-hive agent — local agent management
|
|
1170
|
+
|
|
1171
|
+
Usage:
|
|
1172
|
+
kitty-hive agent <subcommand> [args]
|
|
1173
|
+
|
|
1174
|
+
Subcommands:
|
|
1175
|
+
list List all agents (local + remote placeholders)
|
|
1176
|
+
rename [old] [new] Rename an agent (interactive when args omitted)
|
|
1177
|
+
remove [name-or-id] Remove an agent and all related data (interactive when omitted)`);
|
|
1178
|
+
}
|
|
1179
|
+
function showPeerHelp() {
|
|
1180
|
+
console.log(`🐝 kitty-hive peer — federation peer management
|
|
1181
|
+
|
|
1182
|
+
Usage:
|
|
1183
|
+
kitty-hive peer <subcommand> [args]
|
|
1184
|
+
|
|
1185
|
+
Subcommands:
|
|
1186
|
+
invite [--expose <agent>] Create invite token (recommended for cross-machine)
|
|
1187
|
+
accept [<token>] [--expose <agent>] Accept an invite token (auto-handshake)
|
|
1188
|
+
add [<name>] [<url>] [--expose a,b] [--secret s] Add a peer manually
|
|
1189
|
+
list List configured peers
|
|
1190
|
+
expose [<name>] [<id1,id2,...> | --clear] View/manage exposed agents (TTY → multiselect; non-TTY → show)
|
|
1191
|
+
set-url [<name>] [<url>] Update a peer's URL when auto-sync missed it
|
|
1192
|
+
remove [<name>] Remove a peer
|
|
1193
|
+
|
|
1194
|
+
All subcommands prompt for missing arguments interactively.
|
|
1195
|
+
Cross-machine setups need \`kitty-hive tunnel start\` running.`);
|
|
1196
|
+
}
|
|
1197
|
+
function showTunnelHelp() {
|
|
1198
|
+
console.log(`🐝 kitty-hive tunnel — Cloudflare tunnel control
|
|
1199
|
+
|
|
1200
|
+
Usage:
|
|
1201
|
+
kitty-hive tunnel <subcommand> [args]
|
|
1202
|
+
|
|
1203
|
+
Subcommands:
|
|
1204
|
+
start [--port 4123] [--name name] Run cloudflared & register the public URL with the hive
|
|
1205
|
+
status [--port 4123] Show the URL currently registered with the hive`);
|
|
1206
|
+
}
|
|
1207
|
+
function showConfigHelp() {
|
|
1208
|
+
console.log(`🐝 kitty-hive config — node configuration
|
|
1209
|
+
|
|
1210
|
+
Usage:
|
|
1211
|
+
kitty-hive config <subcommand> [args]
|
|
1212
|
+
|
|
1213
|
+
Subcommands:
|
|
1214
|
+
set [key] [value] Set a config value (interactive when args omitted)
|
|
1215
|
+
|
|
1216
|
+
Known keys:
|
|
1217
|
+
name Node name (the label peers see for this machine)`);
|
|
1218
|
+
}
|
|
1219
|
+
function showFilesHelp() {
|
|
1220
|
+
console.log(`🐝 kitty-hive files — federation transfer file management
|
|
1221
|
+
|
|
1222
|
+
Usage:
|
|
1223
|
+
kitty-hive files <subcommand> [args]
|
|
1224
|
+
|
|
1225
|
+
Subcommands:
|
|
1226
|
+
clean [--days 7] Remove federation transfer files older than N days`);
|
|
1227
|
+
}
|
|
1228
|
+
function showDbHelp() {
|
|
1229
|
+
console.log(`🐝 kitty-hive db — database maintenance
|
|
1230
|
+
|
|
1231
|
+
Usage:
|
|
1232
|
+
kitty-hive db <subcommand> [args]
|
|
1233
|
+
|
|
1234
|
+
Subcommands:
|
|
1235
|
+
clear [--db path] Delete the SQLite database (asks confirmation)`);
|
|
976
1236
|
}
|
|
977
1237
|
// --- Main ---
|
|
1238
|
+
function run(fn) {
|
|
1239
|
+
fn().catch(err => { console.error('Failed:', err); process.exit(1); });
|
|
1240
|
+
}
|
|
978
1241
|
switch (command) {
|
|
979
1242
|
case 'serve':
|
|
980
|
-
cmdServe
|
|
1243
|
+
run(cmdServe);
|
|
981
1244
|
break;
|
|
982
1245
|
case 'init':
|
|
983
|
-
cmdInit
|
|
1246
|
+
run(cmdInit);
|
|
984
1247
|
break;
|
|
985
1248
|
case 'status':
|
|
986
|
-
cmdStatus
|
|
1249
|
+
run(cmdStatus);
|
|
987
1250
|
break;
|
|
988
1251
|
case 'agent':
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1252
|
+
switch (args[1]) {
|
|
1253
|
+
case 'remove':
|
|
1254
|
+
run(cmdAgentRemove);
|
|
1255
|
+
break;
|
|
1256
|
+
case 'rename':
|
|
1257
|
+
run(cmdAgentRename);
|
|
1258
|
+
break;
|
|
1259
|
+
case 'list':
|
|
1260
|
+
run(cmdAgentList);
|
|
1261
|
+
break;
|
|
1262
|
+
default:
|
|
1263
|
+
showAgentHelp();
|
|
1264
|
+
break;
|
|
1000
1265
|
}
|
|
1001
1266
|
break;
|
|
1002
1267
|
case 'peer':
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1268
|
+
switch (args[1]) {
|
|
1269
|
+
case 'add':
|
|
1270
|
+
run(cmdPeerAdd);
|
|
1271
|
+
break;
|
|
1272
|
+
case 'list':
|
|
1273
|
+
run(cmdPeerList);
|
|
1274
|
+
break;
|
|
1275
|
+
case 'remove':
|
|
1276
|
+
run(cmdPeerRemove);
|
|
1277
|
+
break;
|
|
1278
|
+
case 'expose':
|
|
1279
|
+
run(cmdPeerExpose);
|
|
1280
|
+
break;
|
|
1281
|
+
case 'set-url':
|
|
1282
|
+
run(cmdPeerSetUrl);
|
|
1283
|
+
break;
|
|
1284
|
+
case 'invite':
|
|
1285
|
+
run(cmdPeerInvite);
|
|
1286
|
+
break;
|
|
1287
|
+
case 'accept':
|
|
1288
|
+
run(cmdPeerAccept);
|
|
1289
|
+
break;
|
|
1290
|
+
default:
|
|
1291
|
+
showPeerHelp();
|
|
1292
|
+
break;
|
|
1026
1293
|
}
|
|
1027
1294
|
break;
|
|
1028
1295
|
case 'config':
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1296
|
+
switch (args[1]) {
|
|
1297
|
+
case 'set':
|
|
1298
|
+
run(cmdConfigSet);
|
|
1299
|
+
break;
|
|
1300
|
+
default:
|
|
1301
|
+
showConfigHelp();
|
|
1302
|
+
break;
|
|
1034
1303
|
}
|
|
1035
1304
|
break;
|
|
1036
1305
|
case 'db':
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1306
|
+
switch (args[1]) {
|
|
1307
|
+
case 'clear':
|
|
1308
|
+
run(cmdDbClear);
|
|
1309
|
+
break;
|
|
1310
|
+
default:
|
|
1311
|
+
showDbHelp();
|
|
1312
|
+
break;
|
|
1042
1313
|
}
|
|
1043
1314
|
break;
|
|
1044
1315
|
case 'files':
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1316
|
+
switch (args[1]) {
|
|
1317
|
+
case 'clean':
|
|
1318
|
+
run(cmdFilesClean);
|
|
1319
|
+
break;
|
|
1320
|
+
default:
|
|
1321
|
+
showFilesHelp();
|
|
1322
|
+
break;
|
|
1050
1323
|
}
|
|
1051
1324
|
break;
|
|
1052
1325
|
case 'tunnel':
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1326
|
+
switch (args[1]) {
|
|
1327
|
+
case 'start':
|
|
1328
|
+
run(cmdTunnelStart);
|
|
1329
|
+
break;
|
|
1330
|
+
case 'status':
|
|
1331
|
+
run(cmdTunnelStatus);
|
|
1332
|
+
break;
|
|
1333
|
+
default:
|
|
1334
|
+
showTunnelHelp();
|
|
1335
|
+
break;
|
|
1061
1336
|
}
|
|
1062
1337
|
break;
|
|
1063
|
-
|
|
1064
|
-
|
|
1338
|
+
case 'help':
|
|
1339
|
+
case '--help':
|
|
1340
|
+
case '-h':
|
|
1341
|
+
case undefined:
|
|
1342
|
+
showRootHelp();
|
|
1065
1343
|
break;
|
|
1344
|
+
default:
|
|
1345
|
+
console.log(`Unknown command: "${command}"\n`);
|
|
1346
|
+
showRootHelp();
|
|
1347
|
+
process.exit(1);
|
|
1066
1348
|
}
|
|
1067
1349
|
//# sourceMappingURL=index.js.map
|