kitty-hive 0.6.1 → 0.6.3

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
@@ -334,39 +334,197 @@ async function cmdDbClear() {
334
334
  }
335
335
  async function cmdAgentRemove() {
336
336
  const { dbPath } = parseFlags(1);
337
- const target = args[2];
337
+ // Flags: --key <K> look up by external_key (idempotent: missing = exit 0)
338
+ // --yes skip confirmation (scripts)
339
+ // --transfer-to <a> transfer hosted teams to this agent before deleting
340
+ // --cascade explicitly accept that hosted teams (and all their members' history) will be wiped
341
+ let target = '';
342
+ let externalKey = '';
343
+ let assumeYes = false;
344
+ let transferTo = '';
345
+ let cascade = false;
346
+ for (let i = 2; i < args.length; i++) {
347
+ if (args[i] === '--key' && args[i + 1]) {
348
+ externalKey = args[i + 1];
349
+ i++;
350
+ }
351
+ else if (args[i] === '--yes' || args[i] === '-y') {
352
+ assumeYes = true;
353
+ }
354
+ else if (args[i] === '--transfer-to' && args[i + 1]) {
355
+ transferTo = args[i + 1];
356
+ i++;
357
+ }
358
+ else if (args[i] === '--cascade') {
359
+ cascade = true;
360
+ }
361
+ else if (args[i] === '--port' || args[i] === '-p' || args[i] === '--db') {
362
+ i++;
363
+ }
364
+ else if (!args[i].startsWith('-') && !target)
365
+ target = args[i];
366
+ }
338
367
  const db = initDB(dbPath);
339
368
  let agent;
340
- if (!target) {
369
+ if (externalKey) {
370
+ // Idempotent path for orchestrators (kitty/tmux/...). Missing = success.
371
+ const row = db.prepare('SELECT id, display_name FROM agents WHERE external_key = ?').get(externalKey);
372
+ if (!row) {
373
+ console.log(`No agent with external_key="${externalKey}" — nothing to do.`);
374
+ process.exit(0);
375
+ }
376
+ agent = row;
377
+ }
378
+ else if (target) {
379
+ // ID is unique → try ID first; only fall back to display_name match if no ID hit.
380
+ // This avoids a collision where someone registered an agent whose display_name
381
+ // is literally another agent's ID.
382
+ const byId = db.prepare('SELECT id, display_name FROM agents WHERE id = ?').get(target);
383
+ if (byId) {
384
+ agent = byId;
385
+ }
386
+ else {
387
+ const matches = db.prepare('SELECT id, display_name FROM agents WHERE display_name = ?').all(target);
388
+ if (matches.length === 0) {
389
+ console.log(`Agent "${target}" not found.`);
390
+ process.exit(1);
391
+ }
392
+ if (matches.length > 1) {
393
+ console.log(`"${target}" matches ${matches.length} agents. Use id to disambiguate.`);
394
+ for (const m of matches)
395
+ console.log(` ${m.id} ${m.display_name}`);
396
+ process.exit(1);
397
+ }
398
+ agent = matches[0];
399
+ }
400
+ }
401
+ else {
341
402
  if (!isInteractive()) {
342
- console.log('Usage: kitty-hive agent remove <name-or-id>');
403
+ console.log('Usage: kitty-hive agent remove <name-or-id> | --key <key> [--yes] [--transfer-to <agent>] [--cascade]');
343
404
  process.exit(1);
344
405
  }
345
406
  const id = await pickLocalAgent(db, 'Remove which agent?');
346
407
  agent = db.prepare('SELECT id, display_name FROM agents WHERE id = ?').get(id);
347
408
  }
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);
409
+ if (!agent) {
410
+ console.log('Not found.');
411
+ process.exit(1);
412
+ }
413
+ const name = agent.display_name;
414
+ // --- Hosted-team pre-check ---
415
+ const dbMod = await import('./db.js');
416
+ const hostedTeams = dbMod.listTeamsHostedBy(agent.id);
417
+ const hostedRows = hostedTeams.map(t => {
418
+ const members = dbMod.getTeamMembers(t.id);
419
+ return {
420
+ team: t,
421
+ memberCount: members.length,
422
+ otherMemberCount: members.filter(m => m.agent_id !== agent.id).length,
423
+ };
424
+ });
425
+ if (hostedRows.length > 0) {
426
+ console.log(`\n⚠ Agent "${name}" hosts ${hostedRows.length} team(s):`);
427
+ for (const r of hostedRows) {
428
+ console.log(` • ${r.team.name} (${r.team.id}) — ${r.memberCount} member(s), ${r.otherMemberCount} other`);
353
429
  }
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}`);
430
+ console.log('');
431
+ }
432
+ // Decision is only required when at least one hosted team has *other* members.
433
+ // A team where the leaving agent is the sole member is safe to cascade-delete silently
434
+ // (no other history is destroyed), but we still confirm overall removal below.
435
+ let transferAgent;
436
+ const needsHostDecision = hostedRows.some(r => r.otherMemberCount > 0);
437
+ if (needsHostDecision) {
438
+ if (transferTo) {
439
+ const t = db.prepare('SELECT id, display_name FROM agents WHERE id = ? OR display_name = ?').get(transferTo, transferTo);
440
+ if (!t) {
441
+ console.error(`--transfer-to: agent "${transferTo}" not found.`);
442
+ process.exit(1);
443
+ }
444
+ transferAgent = t;
445
+ }
446
+ else if (!cascade && !assumeYes && isInteractive()) {
447
+ // Repeat the team listing inside the prompt itself — the warning lines
448
+ // printed earlier may be scrolled off-screen by clack's render area.
449
+ const teamLines = hostedRows.map(r => {
450
+ const marker = r.otherMemberCount > 0 ? '⚠' : ' ';
451
+ return ` ${marker} ${r.team.name} (${r.memberCount} member${r.memberCount === 1 ? '' : 's'}${r.otherMemberCount > 0 ? `, ${r.otherMemberCount} other` : ''})`;
452
+ }).join('\n');
453
+ const promptMsg = `Agent "${name}" hosts ${hostedRows.length} team(s):\n${teamLines}\n\nHow should they be handled?`;
454
+ const choice = await askSelect({
455
+ message: promptMsg,
456
+ options: [
457
+ { value: 'transfer', label: 'Transfer host to another local agent', hint: 'keeps team history & members' },
458
+ { value: 'cascade', label: 'Delete teams (and all their members + history)', hint: 'destructive, irreversible' },
459
+ { value: 'abort', label: 'Cancel — do nothing' },
460
+ ],
461
+ initialValue: 'transfer',
462
+ });
463
+ if (choice === 'abort') {
464
+ console.log('Cancelled.');
465
+ process.exit(0);
466
+ }
467
+ if (choice === 'transfer') {
468
+ const id = await pickLocalAgent(db, 'Transfer host to which agent?');
469
+ if (id === agent.id) {
470
+ console.error('Cannot transfer to self.');
471
+ process.exit(1);
472
+ }
473
+ transferAgent = db.prepare('SELECT id, display_name FROM agents WHERE id = ?').get(id);
474
+ }
475
+ else {
476
+ cascade = true;
477
+ }
478
+ }
479
+ else if (!cascade) {
480
+ console.error('Hosted teams detected. Pass --transfer-to <agent> to keep them, or --cascade to delete them with their members + history.');
358
481
  process.exit(1);
359
482
  }
360
- agent = matches[0];
361
483
  }
362
- const name = agent.display_name;
363
- const ok = await askConfirm({
364
- message: `Remove agent "${name}" (${agent.id}) and all related data?`,
365
- initialValue: false,
366
- });
367
- if (!ok) {
368
- console.log('Cancelled.');
369
- process.exit(0);
484
+ if (transferAgent && transferAgent.id === agent.id) {
485
+ console.error('Cannot transfer hosted teams to the agent being removed.');
486
+ process.exit(1);
487
+ }
488
+ if (!assumeYes) {
489
+ const summary = transferAgent
490
+ ? `Remove agent "${name}" (${agent.id}); transfer ${hostedRows.length} hosted team(s) to "${transferAgent.display_name}".`
491
+ : hostedRows.length > 0
492
+ ? `Remove agent "${name}" AND cascade-delete ${hostedRows.length} hosted team(s) with all members + history?`
493
+ : `Remove agent "${name}" (${agent.id}) and all related data?`;
494
+ const ok = await askConfirm({ message: summary, initialValue: false });
495
+ if (!ok) {
496
+ console.log('Cancelled.');
497
+ process.exit(0);
498
+ }
499
+ }
500
+ // --- Apply transfer first (so we don't cascade-delete those teams below) ---
501
+ if (transferAgent) {
502
+ const sessionsMod = await import('./sessions.js');
503
+ for (const r of hostedRows) {
504
+ // Make sure new host is a member; if not, add them.
505
+ if (!dbMod.isTeamMember(r.team.id, transferAgent.id)) {
506
+ dbMod.addTeamMember(r.team.id, transferAgent.id);
507
+ }
508
+ dbMod.transferTeamHost(r.team.id, transferAgent.id);
509
+ dbMod.appendTeamEvent(r.team.id, 'host-transfer', null, {
510
+ from_agent_id: agent.id,
511
+ from_display_name: name,
512
+ to_agent_id: transferAgent.id,
513
+ to_display_name: transferAgent.display_name,
514
+ reason: 'agent-remove',
515
+ });
516
+ try {
517
+ await sessionsMod.notifyTeamMembers(r.team.id, undefined, JSON.stringify({
518
+ type: 'team-host-transfer',
519
+ team_id: r.team.id,
520
+ team_name: r.team.name,
521
+ from: name,
522
+ to: transferAgent.display_name,
523
+ }));
524
+ }
525
+ catch { /* push best-effort */ }
526
+ }
527
+ console.log(`↪ Transferred ${hostedRows.length} hosted team(s) to "${transferAgent.display_name}".`);
370
528
  }
371
529
  // Delete in dependency order to satisfy foreign keys
372
530
  db.prepare('DELETE FROM read_cursors WHERE agent_id = ?').run(agent.id);
@@ -380,7 +538,7 @@ async function cmdAgentRemove() {
380
538
  // Team membership + events
381
539
  db.prepare('DELETE FROM team_members WHERE agent_id = ?').run(agent.id);
382
540
  db.prepare('DELETE FROM team_events WHERE actor_agent_id = ?').run(agent.id);
383
- // Teams hosted by this agent (and their content)
541
+ // Teams hosted by this agent (only those left — transferred ones now have a different host)
384
542
  db.prepare(`DELETE FROM team_events WHERE team_id IN (SELECT id FROM teams WHERE host_agent_id = ?)`).run(agent.id);
385
543
  db.prepare(`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE host_agent_id = ?)`).run(agent.id);
386
544
  db.prepare(`DELETE FROM read_cursors WHERE target_id IN (SELECT id FROM teams WHERE host_agent_id = ?)`).run(agent.id);
@@ -388,6 +546,68 @@ async function cmdAgentRemove() {
388
546
  db.prepare('DELETE FROM agents WHERE id = ?').run(agent.id);
389
547
  console.log(`✅ Removed agent "${name}".`);
390
548
  }
549
+ async function cmdAgentRegister() {
550
+ // Idempotent upsert by external_key. Designed for `spawn('kitty-hive',
551
+ // ['agent', 'register', ...])` from session managers / orchestrators.
552
+ // On success prints the agent_id (one line, no decoration) to stdout
553
+ // so callers can pipe it; everything else goes to stderr.
554
+ const { dbPath } = parseFlags(1);
555
+ let externalKey = '';
556
+ let displayName = '';
557
+ let roles = '';
558
+ let tool = '';
559
+ let agentIdOverride = '';
560
+ for (let i = 2; i < args.length; i++) {
561
+ if (args[i] === '--key' && args[i + 1]) {
562
+ externalKey = args[i + 1];
563
+ i++;
564
+ }
565
+ else if (args[i] === '--display-name' && args[i + 1]) {
566
+ displayName = args[i + 1];
567
+ i++;
568
+ }
569
+ else if (args[i] === '--name' && args[i + 1]) {
570
+ displayName = args[i + 1];
571
+ i++;
572
+ }
573
+ else if (args[i] === '--roles' && args[i + 1]) {
574
+ roles = args[i + 1];
575
+ i++;
576
+ }
577
+ else if (args[i] === '--tool' && args[i + 1]) {
578
+ tool = args[i + 1];
579
+ i++;
580
+ }
581
+ else if (args[i] === '--id' && args[i + 1]) {
582
+ agentIdOverride = args[i + 1];
583
+ i++;
584
+ }
585
+ }
586
+ if (!externalKey && !agentIdOverride && !displayName) {
587
+ console.error('Usage: kitty-hive agent register --key <K> --display-name <N> [--roles R] [--tool T]');
588
+ console.error(' All three of --key/--id/--display-name optional, but at least one required.');
589
+ process.exit(1);
590
+ }
591
+ initDB(dbPath);
592
+ const { handleStart } = await import('./tools/start.js');
593
+ try {
594
+ const result = handleStart({
595
+ key: externalKey || undefined,
596
+ id: agentIdOverride || undefined,
597
+ name: displayName || undefined,
598
+ roles: roles || undefined,
599
+ tool: tool || undefined,
600
+ });
601
+ // Stdout: one line, just the agent_id (script-friendly)
602
+ console.log(result.agent_id);
603
+ // Stderr: human context
604
+ console.error(`✅ ${result.display_name} (${result.agent_id})${externalKey ? ` key=${externalKey}` : ''}`);
605
+ }
606
+ catch (err) {
607
+ console.error(`Failed: ${err.message ?? err}`);
608
+ process.exit(1);
609
+ }
610
+ }
391
611
  async function cmdAgentRename() {
392
612
  const { dbPath } = parseFlags(1);
393
613
  const oldName = args[2];
@@ -404,17 +624,25 @@ async function cmdAgentRename() {
404
624
  oldDisplay = db.prepare('SELECT display_name FROM agents WHERE id = ?').get(agentId).display_name;
405
625
  }
406
626
  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);
627
+ // ID-first to avoid collision with display_name that equals some agent's ID.
628
+ const byId = db.prepare('SELECT id, display_name FROM agents WHERE id = ?').get(oldName);
629
+ if (byId) {
630
+ agentId = byId.id;
631
+ oldDisplay = byId.display_name;
411
632
  }
412
- if (matches.length > 1) {
413
- console.log(`"${oldName}" matches ${matches.length} agents. Use agent id to disambiguate.`);
414
- process.exit(1);
633
+ else {
634
+ const matches = db.prepare('SELECT id, display_name FROM agents WHERE display_name = ?').all(oldName);
635
+ if (matches.length === 0) {
636
+ console.log(`Agent "${oldName}" not found.`);
637
+ process.exit(1);
638
+ }
639
+ if (matches.length > 1) {
640
+ console.log(`"${oldName}" matches ${matches.length} agents. Use agent id to disambiguate.`);
641
+ process.exit(1);
642
+ }
643
+ agentId = matches[0].id;
644
+ oldDisplay = matches[0].display_name;
415
645
  }
416
- agentId = matches[0].id;
417
- oldDisplay = matches[0].display_name;
418
646
  }
419
647
  if (!newName) {
420
648
  if (!isInteractive()) {
@@ -440,6 +668,166 @@ async function cmdAgentList() {
440
668
  }
441
669
  console.log(renderTable(AGENT_HEADERS, agentRows(db, agents)));
442
670
  }
671
+ // --- Team commands (CLI-only; agents use hive-team-* MCP tools instead) ---
672
+ async function cmdTeamList() {
673
+ const { dbPath } = parseFlags(1);
674
+ initDB(dbPath);
675
+ const dbMod = await import('./db.js');
676
+ const teams = dbMod.listTeams(false);
677
+ if (teams.length === 0) {
678
+ console.log('No teams.');
679
+ return;
680
+ }
681
+ for (const t of teams) {
682
+ const host = t.host_agent_id ? (getAgentById(t.host_agent_id)?.display_name ?? t.host_agent_id.slice(-12)) : '(none)';
683
+ const memberCount = dbMod.countTeamMembers(t.id);
684
+ const status = t.closed_at ? 'closed' : 'active';
685
+ console.log(`${t.id} ${t.name} host=${host} members=${memberCount} ${status}`);
686
+ }
687
+ }
688
+ async function cmdTeamTransfer() {
689
+ const { dbPath } = parseFlags(1);
690
+ let teamArg = '';
691
+ let toArg = '';
692
+ let assumeYes = false;
693
+ let addMember = false;
694
+ for (let i = 2; i < args.length; i++) {
695
+ if (args[i] === '--to' && args[i + 1]) {
696
+ toArg = args[i + 1];
697
+ i++;
698
+ }
699
+ else if (args[i] === '--yes' || args[i] === '-y') {
700
+ assumeYes = true;
701
+ }
702
+ else if (args[i] === '--add-member') {
703
+ addMember = true;
704
+ }
705
+ else if (args[i] === '--port' || args[i] === '-p' || args[i] === '--db') {
706
+ i++;
707
+ }
708
+ else if (!args[i].startsWith('-') && !teamArg)
709
+ teamArg = args[i];
710
+ }
711
+ const db = initDB(dbPath);
712
+ const dbMod = await import('./db.js');
713
+ // --- Resolve team ---
714
+ let team;
715
+ if (teamArg) {
716
+ team = dbMod.getTeamById(teamArg) || dbMod.getTeamByName(teamArg);
717
+ if (!team) {
718
+ console.error(`Team "${teamArg}" not found.`);
719
+ process.exit(1);
720
+ }
721
+ }
722
+ else {
723
+ if (!isInteractive()) {
724
+ console.error('Usage: kitty-hive team transfer <team> --to <agent> [--add-member] [--yes]');
725
+ process.exit(1);
726
+ }
727
+ const teams = dbMod.listTeams(false);
728
+ if (teams.length === 0) {
729
+ console.log('No teams to transfer.');
730
+ process.exit(0);
731
+ }
732
+ const teamId = await askSelect({
733
+ message: 'Transfer which team?',
734
+ options: teams.map(t => ({
735
+ value: t.id,
736
+ label: t.name,
737
+ hint: `host=${t.host_agent_id ? (getAgentById(t.host_agent_id)?.display_name ?? t.host_agent_id.slice(-12)) : '(none)'} · ${dbMod.countTeamMembers(t.id)} member(s)`,
738
+ })),
739
+ });
740
+ team = teams.find(t => t.id === teamId);
741
+ }
742
+ const currentHostName = team.host_agent_id ? (getAgentById(team.host_agent_id)?.display_name ?? team.host_agent_id.slice(-12)) : '(none)';
743
+ console.log(`\nTeam: ${team.name} (${team.id})\nCurrent host: ${currentHostName}\n`);
744
+ // --- Resolve new host ---
745
+ let newHost;
746
+ if (toArg) {
747
+ // ID-first: ID is unique, only fall back to display_name lookup on no ID hit.
748
+ const byId = db.prepare("SELECT id, display_name FROM agents WHERE id = ? AND origin_peer = ''").get(toArg);
749
+ if (byId) {
750
+ newHost = byId;
751
+ }
752
+ else {
753
+ const matches = db.prepare("SELECT id, display_name FROM agents WHERE display_name = ? AND origin_peer = ''").all(toArg);
754
+ if (matches.length === 0) {
755
+ console.error(`--to: local agent "${toArg}" not found.`);
756
+ process.exit(1);
757
+ }
758
+ if (matches.length > 1) {
759
+ console.error(`--to "${toArg}" matches ${matches.length} agents. Use id to disambiguate.`);
760
+ process.exit(1);
761
+ }
762
+ newHost = matches[0];
763
+ }
764
+ }
765
+ else {
766
+ if (!isInteractive()) {
767
+ console.error('Missing --to <agent>.');
768
+ process.exit(1);
769
+ }
770
+ const id = await pickLocalAgent(db, 'New host?');
771
+ newHost = db.prepare('SELECT id, display_name FROM agents WHERE id = ?').get(id);
772
+ }
773
+ if (!newHost) {
774
+ console.error('New host not found.');
775
+ process.exit(1);
776
+ }
777
+ if (newHost.id === team.host_agent_id) {
778
+ console.log(`"${newHost.display_name}" is already the host. Nothing to do.`);
779
+ process.exit(0);
780
+ }
781
+ // --- Membership check ---
782
+ if (!dbMod.isTeamMember(team.id, newHost.id)) {
783
+ if (!addMember && !assumeYes && isInteractive()) {
784
+ const ok = await askConfirm({
785
+ message: `"${newHost.display_name}" is not a member of "${team.name}". Add them as a member?`,
786
+ initialValue: true,
787
+ });
788
+ if (!ok) {
789
+ console.log('Cancelled.');
790
+ process.exit(0);
791
+ }
792
+ addMember = true;
793
+ }
794
+ else if (!addMember) {
795
+ console.error(`"${newHost.display_name}" is not a member of "${team.name}". Re-run with --add-member to auto-join them, or have them join first.`);
796
+ process.exit(1);
797
+ }
798
+ dbMod.addTeamMember(team.id, newHost.id);
799
+ }
800
+ if (!assumeYes) {
801
+ const ok = await askConfirm({
802
+ message: `Transfer host of "${team.name}" from ${currentHostName} → ${newHost.display_name}?`,
803
+ initialValue: true,
804
+ });
805
+ if (!ok) {
806
+ console.log('Cancelled.');
807
+ process.exit(0);
808
+ }
809
+ }
810
+ dbMod.transferTeamHost(team.id, newHost.id);
811
+ dbMod.appendTeamEvent(team.id, 'host-transfer', null, {
812
+ from_agent_id: team.host_agent_id,
813
+ from_display_name: currentHostName,
814
+ to_agent_id: newHost.id,
815
+ to_display_name: newHost.display_name,
816
+ reason: 'team-transfer',
817
+ });
818
+ try {
819
+ const sessionsMod = await import('./sessions.js');
820
+ await sessionsMod.notifyTeamMembers(team.id, undefined, JSON.stringify({
821
+ type: 'team-host-transfer',
822
+ team_id: team.id,
823
+ team_name: team.name,
824
+ from: currentHostName,
825
+ to: newHost.display_name,
826
+ }));
827
+ }
828
+ catch { /* push best-effort */ }
829
+ console.log(`✅ Host of "${team.name}" → ${newHost.display_name}.`);
830
+ }
443
831
  // --- Node config ---
444
832
  function getConfigPath() {
445
833
  return join(homedir(), '.kitty-hive', 'config.json');
@@ -1341,7 +1729,8 @@ Top-level commands:
1341
1729
  status [--port 4123] Server & agent status
1342
1730
 
1343
1731
  Command groups:
1344
- agent Manage local agents (list, rename, remove)
1732
+ agent Manage local agents (list, rename, remove, register)
1733
+ team Inspect/maintain teams (list, transfer)
1345
1734
  peer Manage federation peers (invite, accept, add, list, expose, set-url, remove)
1346
1735
  log Inspect message history (dm, team, task)
1347
1736
  tunnel Cloudflare tunnel control (start, status)
@@ -1359,9 +1748,27 @@ Usage:
1359
1748
  kitty-hive agent <subcommand> [args]
1360
1749
 
1361
1750
  Subcommands:
1362
- list List all agents (local + remote placeholders)
1363
- rename [old] [new] Rename an agent (interactive when args omitted)
1364
- remove [name-or-id] Remove an agent and all related data (interactive when omitted)`);
1751
+ list List all agents (local + remote placeholders)
1752
+ rename [old] [new] Rename an agent (interactive when args omitted)
1753
+ remove [name-or-id] | --key <K> [--yes] Remove an agent. --key is idempotent (missing = exit 0).
1754
+ Hosted teams: pass --transfer-to <agent> to keep them,
1755
+ or --cascade to delete them with all members + history.
1756
+ register --key <K> --display-name <N> [--roles R] Idempotent upsert by external_key.
1757
+ Stdout = agent_id (one line). Designed for orchestrator scripts.`);
1758
+ }
1759
+ function showTeamHelp() {
1760
+ console.log(`🐝 kitty-hive team — local team maintenance
1761
+
1762
+ Usage:
1763
+ kitty-hive team <subcommand> [args]
1764
+
1765
+ Subcommands:
1766
+ list List all teams with host + member counts
1767
+ transfer [<team>] --to <agent> [--add-member] Transfer team host to another local agent.
1768
+ --add-member auto-joins target if not yet a member.
1769
+
1770
+ Note: agents normally manage teams via the hive-team-* MCP tools.
1771
+ This group is for operator-level maintenance (e.g. before removing a host agent).`);
1365
1772
  }
1366
1773
  function showPeerHelp() {
1367
1774
  console.log(`🐝 kitty-hive peer — federation peer management
@@ -1450,6 +1857,9 @@ switch (command) {
1450
1857
  break;
1451
1858
  case 'agent':
1452
1859
  switch (args[1]) {
1860
+ case 'register':
1861
+ run(cmdAgentRegister);
1862
+ break;
1453
1863
  case 'remove':
1454
1864
  run(cmdAgentRemove);
1455
1865
  break;
@@ -1464,6 +1874,19 @@ switch (command) {
1464
1874
  break;
1465
1875
  }
1466
1876
  break;
1877
+ case 'team':
1878
+ switch (args[1]) {
1879
+ case 'list':
1880
+ run(cmdTeamList);
1881
+ break;
1882
+ case 'transfer':
1883
+ run(cmdTeamTransfer);
1884
+ break;
1885
+ default:
1886
+ showTeamHelp();
1887
+ break;
1888
+ }
1889
+ break;
1467
1890
  case 'peer':
1468
1891
  switch (args[1]) {
1469
1892
  case 'add':