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/README.md +62 -35
- package/README.zh.md +62 -35
- package/channel.ts +26 -4
- package/dist/db.d.ts +17 -1
- package/dist/db.js +89 -8
- package/dist/db.js.map +1 -1
- package/dist/index.js +458 -35
- package/dist/index.js.map +1 -1
- package/dist/mcp/agent-tools.js +6 -3
- package/dist/mcp/agent-tools.js.map +1 -1
- package/dist/models.d.ts +2 -1
- package/dist/models.js +1 -1
- package/dist/models.js.map +1 -1
- package/dist/server.js +12 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/start.d.ts +11 -0
- package/dist/tools/start.js +41 -12
- package/dist/tools/start.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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 (
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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 (
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
1363
|
-
rename
|
|
1364
|
-
remove
|
|
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':
|