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/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 confirm = await ask(`Delete ${resolvedPath}? (y/n)`, 'n');
319
- if (confirm.toLowerCase() !== 'y') {
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
- const matches = db.prepare('SELECT id, display_name FROM agents WHERE id = ? OR display_name = ?').all(target, target);
339
- if (matches.length === 0) {
340
- console.log(`Agent "${target}" not found.`);
341
- process.exit(1);
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
- if (matches.length > 1) {
344
- console.log(`"${target}" matches ${matches.length} agents. Use id to disambiguate.`);
345
- for (const m of matches)
346
- console.log(` ${m.id} ${m.display_name}`);
347
- process.exit(1);
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 confirm = await ask(`Remove agent "${name}" and all related data? (y/n)`, 'n');
352
- if (confirm.toLowerCase() !== 'y') {
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
- const newName = args[3];
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
- const matches = db.prepare('SELECT id FROM agents WHERE display_name = ? OR id = ?').all(oldName, oldName);
386
- if (matches.length === 0) {
387
- console.log(`Agent "${oldName}" not found.`);
388
- process.exit(1);
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
- if (matches.length > 1) {
391
- console.log(`"${oldName}" matches ${matches.length} agents. Use agent id to disambiguate.`);
392
- process.exit(1);
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, matches[0].id);
395
- console.log(`✅ Renamed "${oldName}" → "${newName}".`);
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
- console.log('Usage: kitty-hive peer add <name> <url> [--expose agent1,agent2] [--secret s]');
462
- process.exit(1);
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 --expose to add)'}`);
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
- const name = args[2];
589
+ let name = args[2];
590
+ initDB(dbPath);
515
591
  if (!name) {
516
- console.log('Usage: kitty-hive peer remove <name>');
517
- process.exit(1);
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 (!process.stdin.isTTY) {
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 ask(` Node name`, defaultName)).trim() || defaultName;
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
- console.log('Usage: kitty-hive peer invite --expose <my-agent-id>');
698
- console.log(' --expose YOUR local agent that the peer should be allowed to reach');
699
- console.log('');
700
- console.log(' Cross-machine? Run `kitty-hive tunnel start` first; the URL will be picked up');
701
- console.log(' automatically. (Advanced: --url <https://.../mcp> overrides.)');
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
- if (!token || !myExposed) {
756
- console.log('Usage: kitty-hive peer accept <token> --expose <my-agent-id>');
757
- console.log(' --expose YOUR local agent that the inviter should be allowed to reach');
758
- console.log('');
759
- console.log(' Cross-machine? Run `kitty-hive tunnel start` first; the URL will be picked up');
760
- console.log(' automatically. (Advanced: --url <https://.../mcp> overrides.)');
761
- console.log(' (The token already contains the inviter\'s URL, secret, and exposed agent.)');
762
- process.exit(1);
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
- initDB(dbPath);
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
- const peerName = args[2];
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
- const peerName = args[2];
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
- console.log('Usage: kitty-hive peer expose <peer> --add/--remove <agent>');
886
- process.exit(1);
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
- for (let i = 3; i < args.length; i++) {
896
- if (args[i] === '--add' && args[i + 1]) {
897
- const agents = args[i + 1].split(',').map(s => s.trim());
898
- for (const a of agents)
899
- if (!current.includes(a))
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
- else if (args[i] === '--remove' && args[i + 1]) {
904
- const agents = args[i + 1].split(',').map(s => s.trim());
905
- for (const a of agents) {
906
- const idx = current.indexOf(a);
907
- if (idx >= 0)
908
- current.splice(idx, 1);
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
- i++;
1076
+ if (!next.includes(a))
1077
+ next.push(a);
911
1078
  }
912
1079
  }
913
- updatePeerExposed(peerName, current.join(','));
914
- console.log(`✅ Peer "${peerName}" exposed agents: ${current.join(', ') || 'none'}`);
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
- const key = args[2];
931
- const value = args[3];
932
- if (!key || !value) {
933
- console.log('Usage: kitty-hive config set <key> <value>');
934
- console.log(' e.g. kitty-hive config set name marvin');
935
- process.exit(1);
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
- function showHelp() {
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 serve [--port 4123] [--db path] [-v|-q] Start the server
959
- kitty-hive init <tool> [--port 4123] Write MCP config (claude|cursor|vscode|antigravity|all)
960
- kitty-hive status [--port 4123] Server & agent status
961
- kitty-hive agent list List agents
962
- kitty-hive agent rename <old> <new> Rename an agent
963
- kitty-hive agent remove <name> Remove an agent
964
- kitty-hive peer invite --expose <my-agent> Create invite token (recommended)
965
- kitty-hive peer accept <token> --expose <my-agent> Accept an invite token (auto-handshake)
966
- kitty-hive peer add <name> <url> [--expose a,b] [--secret s] Add a peer (manual)
967
- kitty-hive peer list List peers
968
- kitty-hive peer remove <name> Remove a peer
969
- kitty-hive peer expose <name> --add/--remove <agent> Manage exposed agents
970
- kitty-hive peer set-url <name> <url> Manually update a peer's URL (when auto-sync missed it)
971
- kitty-hive config set <key> <value> Set config (e.g. name)
972
- kitty-hive db clear [--db path] Clear the database
973
- kitty-hive files clean [--days 7] Remove old federation transfer files
974
- kitty-hive tunnel start [--port 4123] [--name name] Run cloudflared & register URL with the hive
975
- kitty-hive tunnel status [--port 4123] Show currently registered tunnel URL`);
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().catch(err => { console.error('Failed:', err); process.exit(1); });
1243
+ run(cmdServe);
981
1244
  break;
982
1245
  case 'init':
983
- cmdInit().catch(err => { console.error('Failed:', err); process.exit(1); });
1246
+ run(cmdInit);
984
1247
  break;
985
1248
  case 'status':
986
- cmdStatus().catch(err => { console.error('Failed:', err); process.exit(1); });
1249
+ run(cmdStatus);
987
1250
  break;
988
1251
  case 'agent':
989
- if (args[1] === 'remove') {
990
- cmdAgentRemove().catch(err => { console.error('Failed:', err); process.exit(1); });
991
- }
992
- else if (args[1] === 'rename') {
993
- cmdAgentRename().catch(err => { console.error('Failed:', err); process.exit(1); });
994
- }
995
- else if (args[1] === 'list') {
996
- cmdAgentList().catch(err => { console.error('Failed:', err); process.exit(1); });
997
- }
998
- else {
999
- showHelp();
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
- if (args[1] === 'add') {
1004
- cmdPeerAdd().catch(err => { console.error('Failed:', err); process.exit(1); });
1005
- }
1006
- else if (args[1] === 'list') {
1007
- cmdPeerList().catch(err => { console.error('Failed:', err); process.exit(1); });
1008
- }
1009
- else if (args[1] === 'remove') {
1010
- cmdPeerRemove().catch(err => { console.error('Failed:', err); process.exit(1); });
1011
- }
1012
- else if (args[1] === 'expose') {
1013
- cmdPeerExpose().catch(err => { console.error('Failed:', err); process.exit(1); });
1014
- }
1015
- else if (args[1] === 'set-url') {
1016
- cmdPeerSetUrl().catch(err => { console.error('Failed:', err); process.exit(1); });
1017
- }
1018
- else if (args[1] === 'invite') {
1019
- cmdPeerInvite().catch(err => { console.error('Failed:', err); process.exit(1); });
1020
- }
1021
- else if (args[1] === 'accept') {
1022
- cmdPeerAccept().catch(err => { console.error('Failed:', err); process.exit(1); });
1023
- }
1024
- else {
1025
- showHelp();
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
- if (args[1] === 'set') {
1030
- cmdConfigSet().catch(err => { console.error('Failed:', err); process.exit(1); });
1031
- }
1032
- else {
1033
- showHelp();
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
- if (args[1] === 'clear') {
1038
- cmdDbClear().catch(err => { console.error('Failed:', err); process.exit(1); });
1039
- }
1040
- else {
1041
- showHelp();
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
- if (args[1] === 'clean') {
1046
- cmdFilesClean().catch(err => { console.error('Failed:', err); process.exit(1); });
1047
- }
1048
- else {
1049
- showHelp();
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
- if (args[1] === 'start') {
1054
- cmdTunnelStart().catch(err => { console.error('Failed:', err); process.exit(1); });
1055
- }
1056
- else if (args[1] === 'status') {
1057
- cmdTunnelStatus().catch(err => { console.error('Failed:', err); process.exit(1); });
1058
- }
1059
- else {
1060
- showHelp();
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
- default:
1064
- showHelp();
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