kitty-hive 0.6.11 → 0.7.1
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/channel.ts +10 -1
- package/codex-channel.ts +463 -3
- package/dist/admin-http.js +89 -0
- package/dist/admin-http.js.map +1 -1
- package/dist/codex-channel-runtime.d.ts +126 -0
- package/dist/codex-channel-runtime.js +203 -0
- package/dist/codex-channel-runtime.js.map +1 -0
- package/dist/codex-supervisor.d.ts +66 -0
- package/dist/codex-supervisor.js +317 -0
- package/dist/codex-supervisor.js.map +1 -0
- package/dist/db.d.ts +27 -0
- package/dist/db.js +82 -5
- package/dist/db.js.map +1 -1
- package/dist/index.js +428 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp/agent-tools.js +62 -1
- package/dist/mcp/agent-tools.js.map +1 -1
- package/dist/mcp/dm-tools.js +6 -5
- package/dist/mcp/dm-tools.js.map +1 -1
- package/dist/mcp/server.js +16 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/task-tools.js +30 -10
- package/dist/mcp/task-tools.js.map +1 -1
- package/dist/mcp/team-tools.js +19 -1
- package/dist/mcp/team-tools.js.map +1 -1
- package/dist/models.d.ts +6 -0
- package/dist/models.js.map +1 -1
- package/dist/preview.js +7 -1
- package/dist/preview.js.map +1 -1
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/start.d.ts +1 -0
- package/dist/tools/start.js +55 -12
- package/dist/tools/start.js.map +1 -1
- package/dist/tools/team.d.ts +13 -0
- package/dist/tools/team.js +25 -1
- package/dist/tools/team.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,8 +10,24 @@ import { join, dirname, basename, delimiter } from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { homedir, hostname } from 'node:os';
|
|
12
12
|
import { execSync } from 'node:child_process';
|
|
13
|
+
import { buildPushMessage } from './preview.js';
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = dirname(__filename);
|
|
16
|
+
// Build a properly-formatted push payload from operator-side CLI commands.
|
|
17
|
+
// Operator has no agent identity, so `from` is empty (buildPushMessage drops
|
|
18
|
+
// the "from X" suffix when empty). The push goes through the same code path
|
|
19
|
+
// as MCP-side notifications, so receivers see consistent `[hive] xxx — call
|
|
20
|
+
// hive-... for details.` hints regardless of whether the change came from
|
|
21
|
+
// CLI or an agent calling an MCP tool.
|
|
22
|
+
function operatorPush(opts) {
|
|
23
|
+
return buildPushMessage({
|
|
24
|
+
type: opts.type,
|
|
25
|
+
from: '',
|
|
26
|
+
from_agent_id: '',
|
|
27
|
+
event_id: opts.eventId,
|
|
28
|
+
team_id: opts.teamId,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
15
31
|
const args = process.argv.slice(2);
|
|
16
32
|
const command = args[0];
|
|
17
33
|
function parseFlags(startIdx) {
|
|
@@ -70,6 +86,115 @@ async function cmdCodexChannel() {
|
|
|
70
86
|
// Block until child exits (handled above)
|
|
71
87
|
await new Promise(() => { });
|
|
72
88
|
}
|
|
89
|
+
// codex-pane <subcommand>: launcher-facing utilities for path B (visible codex
|
|
90
|
+
// via `codex --remote`). Currently has one subcommand:
|
|
91
|
+
// ws --key <K> | --id <I> Print the daemon's ws_url + thread_id as JSON,
|
|
92
|
+
// so a launcher (kitty-kitty etc.) can spawn
|
|
93
|
+
// `codex --remote <ws_url>` inside a pane. Polls
|
|
94
|
+
// with exponential backoff until status='ready'
|
|
95
|
+
// or hard timeout.
|
|
96
|
+
async function cmdCodexPane() {
|
|
97
|
+
const sub = args[1];
|
|
98
|
+
if (sub !== 'ws') {
|
|
99
|
+
console.error('Usage: kitty-hive codex-pane ws --key <K> | --id <I> [--port 4123] [--timeout-ms 5000]');
|
|
100
|
+
process.exit(2);
|
|
101
|
+
}
|
|
102
|
+
const { port, dbPath } = parseFlags(1);
|
|
103
|
+
let agentKey = '';
|
|
104
|
+
let agentId = '';
|
|
105
|
+
let timeoutMs = 5000;
|
|
106
|
+
for (let i = 2; i < args.length; i++) {
|
|
107
|
+
if (args[i] === '--key' && args[i + 1]) {
|
|
108
|
+
agentKey = args[i + 1];
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
else if (args[i] === '--id' && args[i + 1]) {
|
|
112
|
+
agentId = args[i + 1];
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
else if (args[i] === '--timeout-ms' && args[i + 1]) {
|
|
116
|
+
timeoutMs = parseInt(args[i + 1], 10) || 5000;
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else if (args[i] === '--port' || args[i] === '-p') {
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
else if (args[i] === '--db') {
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!agentKey && !agentId) {
|
|
127
|
+
console.error('Usage: kitty-hive codex-pane ws --key <K> | --id <I>');
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
// Backoff: 200ms / 400 / 800 / 1600 / 3200 / cap at remaining time
|
|
131
|
+
const start = Date.now();
|
|
132
|
+
const delays = [200, 400, 800, 1600, 3200];
|
|
133
|
+
let attempt = 0;
|
|
134
|
+
let lastBody = null;
|
|
135
|
+
while (true) {
|
|
136
|
+
try {
|
|
137
|
+
const url = new URL(`http://127.0.0.1:${port}/admin/codex-daemons`);
|
|
138
|
+
const res = await fetch(url);
|
|
139
|
+
if (res.ok) {
|
|
140
|
+
const { daemons } = await res.json();
|
|
141
|
+
const match = daemons.find(d => (agentId && d.agent_id === agentId) ||
|
|
142
|
+
// No external_key in admin snapshot, so we have to look up by id.
|
|
143
|
+
// For agent_key path, resolve via DB locally.
|
|
144
|
+
false);
|
|
145
|
+
let chosen = match;
|
|
146
|
+
if (!chosen && agentKey) {
|
|
147
|
+
// Resolve key → id via DB (use the same dbPath as serve uses)
|
|
148
|
+
const db = initDB(dbPath);
|
|
149
|
+
const row = db.prepare('SELECT id FROM agents WHERE external_key = ?').get(agentKey);
|
|
150
|
+
if (row)
|
|
151
|
+
chosen = daemons.find(d => d.agent_id === row.id);
|
|
152
|
+
}
|
|
153
|
+
if (chosen) {
|
|
154
|
+
if (chosen.ready && chosen.ws_url && chosen.thread_id) {
|
|
155
|
+
console.log(JSON.stringify({
|
|
156
|
+
status: 'ready',
|
|
157
|
+
agent_id: chosen.agent_id,
|
|
158
|
+
display_name: chosen.display_name,
|
|
159
|
+
ws_url: chosen.ws_url,
|
|
160
|
+
thread_id: chosen.thread_id,
|
|
161
|
+
pid: chosen.pid,
|
|
162
|
+
}));
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
// daemon row exists but its codex app-server hasn't reported ready
|
|
166
|
+
// back yet — wrap in the contract shape so callers can switch on
|
|
167
|
+
// `status` consistently (no raw daemon snapshot leaking out).
|
|
168
|
+
lastBody = {
|
|
169
|
+
status: 'starting',
|
|
170
|
+
agent_id: chosen.agent_id,
|
|
171
|
+
display_name: chosen.display_name,
|
|
172
|
+
pid: chosen.pid,
|
|
173
|
+
uptime_ms: chosen.uptime_ms,
|
|
174
|
+
restart_count: chosen.restart_count,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
lastBody = { status: 'not_supervised', error: 'no daemon for that agent_key/id (agent not registered with tool=codex, or supervisor not running)' };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
lastBody = { status: 'error', error: String(err?.message || err) };
|
|
184
|
+
}
|
|
185
|
+
if (Date.now() - start >= timeoutMs)
|
|
186
|
+
break;
|
|
187
|
+
const delay = delays[Math.min(attempt, delays.length - 1)];
|
|
188
|
+
attempt++;
|
|
189
|
+
await new Promise(r => setTimeout(r, Math.min(delay, Math.max(50, timeoutMs - (Date.now() - start)))));
|
|
190
|
+
}
|
|
191
|
+
// Timeout — emit what we last saw, exit non-zero
|
|
192
|
+
console.log(JSON.stringify(lastBody || {
|
|
193
|
+
status: 'timeout',
|
|
194
|
+
error: `daemon did not reach ready within ${timeoutMs}ms`,
|
|
195
|
+
}));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
73
198
|
const INIT_TOOLS = ['claude', 'cursor', 'vscode'];
|
|
74
199
|
// codex is config'd via its own CLI (`codex mcp add ... --url ...`) writing
|
|
75
200
|
// to ~/.codex/config.toml — we shell out instead of editing toml ourselves.
|
|
@@ -381,6 +506,36 @@ async function cmdStatus() {
|
|
|
381
506
|
console.log(renderTable(['ID', 'NAME', 'PEER', 'STATUS', 'LAST SEEN'], rrows, ' '));
|
|
382
507
|
}
|
|
383
508
|
}
|
|
509
|
+
// Codex daemon supervisor snapshot — fetched live via admin endpoint
|
|
510
|
+
// so we get the actual running state, not just the DB intent.
|
|
511
|
+
try {
|
|
512
|
+
const daemonsRes = await fetch(`http://localhost:${port}/admin/codex-daemons`);
|
|
513
|
+
if (daemonsRes.ok) {
|
|
514
|
+
const { daemons } = await daemonsRes.json();
|
|
515
|
+
const codexAgents = agents.filter((a) => a.tool === 'codex');
|
|
516
|
+
if (codexAgents.length > 0 || daemons.length > 0) {
|
|
517
|
+
console.log(`\n🤖 Codex daemons`);
|
|
518
|
+
const liveByName = new Map(daemons.map(d => [d.display_name, d]));
|
|
519
|
+
const rows = codexAgents.map((a) => {
|
|
520
|
+
const live = liveByName.get(a.display_name);
|
|
521
|
+
if (live) {
|
|
522
|
+
const min = Math.floor(live.uptime_ms / 60000);
|
|
523
|
+
const uptimeStr = min < 60 ? `${min}m` : `${Math.floor(min / 60)}h${min % 60}m`;
|
|
524
|
+
return [a.display_name, `pid=${live.pid}`, `up ${uptimeStr}`, `restarts=${live.restart_count}`];
|
|
525
|
+
}
|
|
526
|
+
return [a.display_name, '(not running)', '', ''];
|
|
527
|
+
});
|
|
528
|
+
// Also surface zombie daemons (running but agent row deleted)
|
|
529
|
+
for (const d of daemons) {
|
|
530
|
+
if (!codexAgents.some((a) => a.display_name === d.display_name)) {
|
|
531
|
+
rows.push([`${d.display_name} ⚠️ orphan`, `pid=${d.pid}`, '', `restarts=${d.restart_count}`]);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
console.log(renderTable(['NAME', 'PID', 'UPTIME', 'RESTARTS'], rows, ' '));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch { /* serve might not expose admin yet — silently skip */ }
|
|
384
539
|
}
|
|
385
540
|
catch {
|
|
386
541
|
console.log(`\n⚠️ Cannot read database`);
|
|
@@ -409,7 +564,7 @@ async function cmdDbClear() {
|
|
|
409
564
|
console.log('✅ Database cleared.');
|
|
410
565
|
}
|
|
411
566
|
async function cmdAgentRemove() {
|
|
412
|
-
const { dbPath } = parseFlags(1);
|
|
567
|
+
const { dbPath, port } = parseFlags(1);
|
|
413
568
|
// Flags: --key <K> look up by external_key (idempotent: missing = exit 0)
|
|
414
569
|
// --yes skip confirmation (scripts)
|
|
415
570
|
// --transfer-to <a> transfer hosted teams to this agent before deleting
|
|
@@ -590,12 +745,10 @@ async function cmdAgentRemove() {
|
|
|
590
745
|
reason: 'agent-remove',
|
|
591
746
|
});
|
|
592
747
|
try {
|
|
593
|
-
await sessionsMod.notifyTeamMembers(r.team.id, undefined,
|
|
748
|
+
await sessionsMod.notifyTeamMembers(r.team.id, undefined, operatorPush({
|
|
594
749
|
type: 'team-host-transfer',
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
from: name,
|
|
598
|
-
to: transferAgent.display_name,
|
|
750
|
+
teamId: r.team.id,
|
|
751
|
+
eventId: `team:${r.team.id}:host-transfer:${Date.now()}`,
|
|
599
752
|
}));
|
|
600
753
|
}
|
|
601
754
|
catch { /* push best-effort */ }
|
|
@@ -621,6 +774,27 @@ async function cmdAgentRemove() {
|
|
|
621
774
|
db.prepare('DELETE FROM teams WHERE host_agent_id = ?').run(agent.id);
|
|
622
775
|
db.prepare('DELETE FROM agents WHERE id = ?').run(agent.id);
|
|
623
776
|
console.log(`✅ Removed agent "${name}".`);
|
|
777
|
+
// Tell the live supervisor (if any) to kill the daemon for this agent.
|
|
778
|
+
// Without this, a deleted tool=codex agent's daemon keeps running on its
|
|
779
|
+
// old ws_url, causing routing schisms when another agent later self-
|
|
780
|
+
// registers with the same name. Mirrors the agent-register notify path.
|
|
781
|
+
// Best-effort: failures silently ignored — agent row is already gone.
|
|
782
|
+
try {
|
|
783
|
+
const notifyRes = await fetch(`http://127.0.0.1:${port}/admin/notify-agent-removed`, {
|
|
784
|
+
method: 'POST',
|
|
785
|
+
headers: { 'Content-Type': 'application/json' },
|
|
786
|
+
body: JSON.stringify({ agent_id: agent.id }),
|
|
787
|
+
signal: AbortSignal.timeout(2000),
|
|
788
|
+
});
|
|
789
|
+
if (notifyRes.ok) {
|
|
790
|
+
const { killed } = await notifyRes.json();
|
|
791
|
+
if (killed)
|
|
792
|
+
console.error(` → supervisor killed codex daemon`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
// serve not running, or admin endpoint not reachable — fine, no daemon to kill anyway
|
|
797
|
+
}
|
|
624
798
|
}
|
|
625
799
|
async function cmdAgentRegister() {
|
|
626
800
|
// Idempotent upsert by external_key. Designed for `spawn('kitty-hive',
|
|
@@ -633,6 +807,7 @@ async function cmdAgentRegister() {
|
|
|
633
807
|
let roles = '';
|
|
634
808
|
let tool = '';
|
|
635
809
|
let agentIdOverride = '';
|
|
810
|
+
let projectDir = '';
|
|
636
811
|
for (let i = 2; i < args.length; i++) {
|
|
637
812
|
if (args[i] === '--key' && args[i + 1]) {
|
|
638
813
|
externalKey = args[i + 1];
|
|
@@ -658,12 +833,18 @@ async function cmdAgentRegister() {
|
|
|
658
833
|
agentIdOverride = args[i + 1];
|
|
659
834
|
i++;
|
|
660
835
|
}
|
|
836
|
+
else if (args[i] === '--project-dir' && args[i + 1]) {
|
|
837
|
+
projectDir = args[i + 1];
|
|
838
|
+
i++;
|
|
839
|
+
}
|
|
661
840
|
}
|
|
662
841
|
if (!externalKey && !agentIdOverride && !displayName) {
|
|
663
|
-
console.error('Usage: kitty-hive agent register --key <K> --display-name <N> [--roles R] [--tool T]');
|
|
842
|
+
console.error('Usage: kitty-hive agent register --key <K> --display-name <N> [--roles R] [--tool T] [--project-dir P]');
|
|
664
843
|
console.error(' All three of --key/--id/--display-name optional, but at least one required.');
|
|
844
|
+
console.error(' --project-dir: working directory hint for codex agents (passed as cwd when supervisor spawns codex app-server).');
|
|
665
845
|
process.exit(1);
|
|
666
846
|
}
|
|
847
|
+
const { port } = parseFlags(1);
|
|
667
848
|
initDB(dbPath);
|
|
668
849
|
const { handleStart } = await import('./tools/start.js');
|
|
669
850
|
try {
|
|
@@ -673,11 +854,37 @@ async function cmdAgentRegister() {
|
|
|
673
854
|
name: displayName || undefined,
|
|
674
855
|
roles: roles || undefined,
|
|
675
856
|
tool: tool || undefined,
|
|
857
|
+
projectDir: projectDir || undefined,
|
|
676
858
|
});
|
|
677
859
|
// Stdout: one line, just the agent_id (script-friendly)
|
|
678
860
|
console.log(result.agent_id);
|
|
679
861
|
// Stderr: human context
|
|
680
|
-
console.error(`✅ ${result.display_name} (${result.agent_id})${externalKey ? ` key=${externalKey}` : ''}`);
|
|
862
|
+
console.error(`✅ ${result.display_name} (${result.agent_id})${externalKey ? ` key=${externalKey}` : ''}${projectDir ? ` cwd=${projectDir}` : ''}`);
|
|
863
|
+
// If serve is running and we registered a tool=codex agent, notify the
|
|
864
|
+
// supervisor so it can spawn a daemon for it on the fly (without this,
|
|
865
|
+
// dynamic spawn requires a serve restart because the in-process
|
|
866
|
+
// onAgentCreated hook lives in serve's process, not this CLI process).
|
|
867
|
+
// Best-effort: failures are silently ignored — agent row is already written.
|
|
868
|
+
if (tool === 'codex') {
|
|
869
|
+
try {
|
|
870
|
+
const notifyRes = await fetch(`http://127.0.0.1:${port}/admin/notify-agent-created`, {
|
|
871
|
+
method: 'POST',
|
|
872
|
+
headers: { 'Content-Type': 'application/json' },
|
|
873
|
+
body: JSON.stringify({ agent_id: result.agent_id }),
|
|
874
|
+
// Short timeout: register shouldn't block waiting for a dead serve
|
|
875
|
+
signal: AbortSignal.timeout(2000),
|
|
876
|
+
});
|
|
877
|
+
if (notifyRes.ok) {
|
|
878
|
+
const { spawned } = await notifyRes.json();
|
|
879
|
+
if (spawned)
|
|
880
|
+
console.error(` → supervisor spawning codex daemon`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
// serve not running, or admin endpoint not reachable — that's fine,
|
|
885
|
+
// daemon will spawn at the next serve boot.
|
|
886
|
+
}
|
|
887
|
+
}
|
|
681
888
|
}
|
|
682
889
|
catch (err) {
|
|
683
890
|
console.error(`Failed: ${err.message ?? err}`);
|
|
@@ -893,12 +1100,10 @@ async function cmdTeamTransfer() {
|
|
|
893
1100
|
});
|
|
894
1101
|
try {
|
|
895
1102
|
const sessionsMod = await import('./sessions.js');
|
|
896
|
-
await sessionsMod.notifyTeamMembers(team.id, undefined,
|
|
1103
|
+
await sessionsMod.notifyTeamMembers(team.id, undefined, operatorPush({
|
|
897
1104
|
type: 'team-host-transfer',
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
from: currentHostName,
|
|
901
|
-
to: newHost.display_name,
|
|
1105
|
+
teamId: team.id,
|
|
1106
|
+
eventId: `team:${team.id}:host-transfer:${Date.now()}`,
|
|
902
1107
|
}));
|
|
903
1108
|
}
|
|
904
1109
|
catch { /* push best-effort */ }
|
|
@@ -1026,17 +1231,213 @@ async function cmdTeamKick() {
|
|
|
1026
1231
|
});
|
|
1027
1232
|
try {
|
|
1028
1233
|
const sessionsMod = await import('./sessions.js');
|
|
1029
|
-
await sessionsMod.notifyTeamMembers(team.id, undefined,
|
|
1234
|
+
await sessionsMod.notifyTeamMembers(team.id, undefined, operatorPush({
|
|
1030
1235
|
type: 'team-kick',
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
agent_id: target.id,
|
|
1034
|
-
display_name: target.display_name,
|
|
1236
|
+
teamId: team.id,
|
|
1237
|
+
eventId: `team:${team.id}:kick:${Date.now()}`,
|
|
1035
1238
|
}));
|
|
1036
1239
|
}
|
|
1037
1240
|
catch { /* push best-effort */ }
|
|
1038
1241
|
console.log(`✅ Removed "${target.display_name}" from "${team.name}".`);
|
|
1039
1242
|
}
|
|
1243
|
+
// Manage a team's rules (free-form markdown shown to all members on hive_start
|
|
1244
|
+
// and hive_team_info). Operator-side — no MCP tool exposes mutation, so rules
|
|
1245
|
+
// can't be hijacked by a member. Subcommands: show / edit / set / clear.
|
|
1246
|
+
async function cmdTeamRules() {
|
|
1247
|
+
const { dbPath } = parseFlags(2);
|
|
1248
|
+
let teamArg = args[2] && !args[2].startsWith('-') ? args[2] : '';
|
|
1249
|
+
let action = args[3] && !args[3].startsWith('-') ? args[3] : '';
|
|
1250
|
+
const db = initDB(dbPath);
|
|
1251
|
+
const dbMod = await import('./db.js');
|
|
1252
|
+
// --- Resolve team (interactive picker when omitted in a TTY) ---
|
|
1253
|
+
if (!teamArg) {
|
|
1254
|
+
if (!isInteractive()) {
|
|
1255
|
+
console.log('Usage:');
|
|
1256
|
+
console.log(' kitty-hive team rules <team> show (alias: omit subcommand)');
|
|
1257
|
+
console.log(' kitty-hive team rules <team> show');
|
|
1258
|
+
console.log(' kitty-hive team rules <team> edit Open $EDITOR for interactive edit');
|
|
1259
|
+
console.log(' kitty-hive team rules <team> set --file <path> Replace from file');
|
|
1260
|
+
console.log(' kitty-hive team rules <team> set --text "..." Replace from string');
|
|
1261
|
+
console.log(' kitty-hive team rules <team> clear');
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
const teams = dbMod.listTeams(false);
|
|
1265
|
+
if (teams.length === 0) {
|
|
1266
|
+
console.log('No teams.');
|
|
1267
|
+
process.exit(0);
|
|
1268
|
+
}
|
|
1269
|
+
teamArg = await askSelect({
|
|
1270
|
+
message: 'Which team?',
|
|
1271
|
+
options: teams.map(t => ({
|
|
1272
|
+
value: t.id,
|
|
1273
|
+
label: t.name,
|
|
1274
|
+
hint: t.rules ? `${t.rules.length} chars of rules` : '(no rules)',
|
|
1275
|
+
})),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
const team = dbMod.getTeamById(teamArg) || dbMod.getTeamByName(teamArg);
|
|
1279
|
+
if (!team) {
|
|
1280
|
+
console.error(`Team "${teamArg}" not found.`);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
// --- Resolve action (interactive picker when omitted in a TTY) ---
|
|
1284
|
+
if (!action) {
|
|
1285
|
+
if (!isInteractive()) {
|
|
1286
|
+
action = 'show'; // default in scripts: just print
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
action = await askSelect({
|
|
1290
|
+
message: `Rules for "${team.name}" — what do you want to do?`,
|
|
1291
|
+
options: [
|
|
1292
|
+
{ value: 'show', label: 'Show current rules' },
|
|
1293
|
+
{ value: 'edit', label: 'Edit in $EDITOR' },
|
|
1294
|
+
{ value: 'set', label: 'Replace from file or text' },
|
|
1295
|
+
{ value: 'clear', label: 'Clear rules' },
|
|
1296
|
+
],
|
|
1297
|
+
initialValue: team.rules ? 'show' : 'edit',
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (action === 'show') {
|
|
1302
|
+
if (!team.rules) {
|
|
1303
|
+
console.log(`Team "${team.name}" has no rules set.`);
|
|
1304
|
+
console.log(`Set them with: kitty-hive team rules ${team.name} edit`);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
console.log(`# Rules for team "${team.name}"\n`);
|
|
1308
|
+
console.log(team.rules);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (action === 'clear') {
|
|
1312
|
+
let assumeYes = false;
|
|
1313
|
+
for (let i = 4; i < args.length; i++)
|
|
1314
|
+
if (args[i] === '--yes' || args[i] === '-y')
|
|
1315
|
+
assumeYes = true;
|
|
1316
|
+
if (!assumeYes && isInteractive()) {
|
|
1317
|
+
const ok = await askConfirm({ message: `Clear rules for team "${team.name}"?`, initialValue: false });
|
|
1318
|
+
if (!ok) {
|
|
1319
|
+
console.log('Cancelled.');
|
|
1320
|
+
process.exit(0);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
dbMod.setTeamRules(team.id, '');
|
|
1324
|
+
try {
|
|
1325
|
+
const sessionsMod = await import('./sessions.js');
|
|
1326
|
+
await sessionsMod.notifyTeamMembers(team.id, undefined, operatorPush({
|
|
1327
|
+
type: 'team-rules-update',
|
|
1328
|
+
teamId: team.id,
|
|
1329
|
+
eventId: `team:${team.id}:rules-update:${Date.now()}`,
|
|
1330
|
+
}));
|
|
1331
|
+
}
|
|
1332
|
+
catch { /* best-effort */ }
|
|
1333
|
+
console.log(`✅ Cleared rules for "${team.name}".`);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
// Helper: persist new rules + push notification (used by set + edit branches)
|
|
1337
|
+
const applyRules = async (content, label) => {
|
|
1338
|
+
dbMod.setTeamRules(team.id, content);
|
|
1339
|
+
try {
|
|
1340
|
+
const sessionsMod = await import('./sessions.js');
|
|
1341
|
+
await sessionsMod.notifyTeamMembers(team.id, undefined, operatorPush({
|
|
1342
|
+
type: 'team-rules-update',
|
|
1343
|
+
teamId: team.id,
|
|
1344
|
+
eventId: `team:${team.id}:rules-update:${Date.now()}`,
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
1347
|
+
catch { /* best-effort */ }
|
|
1348
|
+
console.log(`✅ ${label === 'set' ? 'Set' : 'Updated'} rules for "${team.name}" (${content.length} chars).`);
|
|
1349
|
+
};
|
|
1350
|
+
if (action === 'set') {
|
|
1351
|
+
let filePath = '';
|
|
1352
|
+
let text = null;
|
|
1353
|
+
for (let i = 4; i < args.length; i++) {
|
|
1354
|
+
if (args[i] === '--file' && args[i + 1]) {
|
|
1355
|
+
filePath = args[i + 1];
|
|
1356
|
+
i++;
|
|
1357
|
+
}
|
|
1358
|
+
else if (args[i] === '--text' && args[i + 1]) {
|
|
1359
|
+
text = args[i + 1];
|
|
1360
|
+
i++;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (filePath) {
|
|
1364
|
+
if (!existsSync(filePath)) {
|
|
1365
|
+
console.error(`File not found: ${filePath}`);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
}
|
|
1368
|
+
await applyRules(readFileSync(filePath, 'utf8'), 'set');
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (text !== null) {
|
|
1372
|
+
await applyRules(text, 'set');
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
// No flags — interactive picker for source (editor / file / text)
|
|
1376
|
+
if (!isInteractive()) {
|
|
1377
|
+
console.error('Usage: kitty-hive team rules <team> set --file <path> | --text "..."');
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1380
|
+
const source = await askSelect({
|
|
1381
|
+
message: 'Where should the new rules come from?',
|
|
1382
|
+
options: [
|
|
1383
|
+
{ value: 'editor', label: 'Open $EDITOR (multi-line markdown)' },
|
|
1384
|
+
{ value: 'file', label: 'Load from file' },
|
|
1385
|
+
{ value: 'text', label: 'Type/paste a one-liner' },
|
|
1386
|
+
],
|
|
1387
|
+
initialValue: 'editor',
|
|
1388
|
+
});
|
|
1389
|
+
if (source === 'editor') {
|
|
1390
|
+
action = 'edit'; // promote — edit handler below picks it up
|
|
1391
|
+
}
|
|
1392
|
+
else if (source === 'file') {
|
|
1393
|
+
const p = await askText({
|
|
1394
|
+
message: 'Path to file?',
|
|
1395
|
+
validate: (v) => ((v ?? '').trim().length === 0 ? 'Path required' : undefined),
|
|
1396
|
+
});
|
|
1397
|
+
if (!existsSync(p)) {
|
|
1398
|
+
console.error(`File not found: ${p}`);
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
await applyRules(readFileSync(p, 'utf8'), 'set');
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
const oneliner = await askText({
|
|
1406
|
+
message: 'New rules (single line):',
|
|
1407
|
+
validate: (v) => ((v ?? '').trim().length === 0 ? 'Rules cannot be empty (use clear instead)' : undefined),
|
|
1408
|
+
});
|
|
1409
|
+
await applyRules(oneliner, 'set');
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (action === 'edit') {
|
|
1414
|
+
if (!isInteractive()) {
|
|
1415
|
+
console.error('`team rules <team> edit` requires a TTY. Use --file or --text in non-interactive contexts.');
|
|
1416
|
+
process.exit(1);
|
|
1417
|
+
}
|
|
1418
|
+
const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === 'win32' ? 'notepad' : 'vi');
|
|
1419
|
+
const tmpPath = join('/tmp', `kitty-hive-team-rules-${team.id.slice(-12)}-${Date.now()}.md`);
|
|
1420
|
+
writeFileSync(tmpPath, team.rules || `# Rules for team "${team.name}"\n\n(Write the team's rules here. Save and exit to apply; quit without saving to abort.)\n`);
|
|
1421
|
+
try {
|
|
1422
|
+
execSync(`${editor} "${tmpPath}"`, { stdio: 'inherit' });
|
|
1423
|
+
const newContent = readFileSync(tmpPath, 'utf8');
|
|
1424
|
+
if (newContent === team.rules) {
|
|
1425
|
+
console.log('No changes — rules unchanged.');
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
await applyRules(newContent, 'updated');
|
|
1429
|
+
}
|
|
1430
|
+
finally {
|
|
1431
|
+
try {
|
|
1432
|
+
unlinkSync(tmpPath);
|
|
1433
|
+
}
|
|
1434
|
+
catch { /* ignore */ }
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
console.error(`Unknown action: "${action}". Use show / edit / set / clear.`);
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1040
1441
|
// --- Node config ---
|
|
1041
1442
|
function getConfigPath() {
|
|
1042
1443
|
return join(homedir(), '.kitty-hive', 'config.json');
|
|
@@ -1978,6 +2379,9 @@ Subcommands:
|
|
|
1978
2379
|
--add-member auto-joins target if not yet a member.
|
|
1979
2380
|
kick [<team>] [<agent>] [--yes] Remove an agent from a team (operator-side).
|
|
1980
2381
|
Cannot kick the host — transfer first.
|
|
2382
|
+
rules <team> [show|edit|set|clear] Manage the team's rules / charter (free-form
|
|
2383
|
+
markdown; surfaced to all members on hive_start
|
|
2384
|
+
+ hive_team_info). Default: show.
|
|
1981
2385
|
|
|
1982
2386
|
Note: agents normally manage teams via the hive-team-* MCP tools.
|
|
1983
2387
|
This group is for operator-level maintenance (e.g. before removing a host agent).`);
|
|
@@ -2067,6 +2471,9 @@ switch (command) {
|
|
|
2067
2471
|
case 'codex-channel':
|
|
2068
2472
|
run(cmdCodexChannel);
|
|
2069
2473
|
break;
|
|
2474
|
+
case 'codex-pane':
|
|
2475
|
+
run(cmdCodexPane);
|
|
2476
|
+
break;
|
|
2070
2477
|
case 'status':
|
|
2071
2478
|
run(cmdStatus);
|
|
2072
2479
|
break;
|
|
@@ -2100,6 +2507,9 @@ switch (command) {
|
|
|
2100
2507
|
case 'kick':
|
|
2101
2508
|
run(cmdTeamKick);
|
|
2102
2509
|
break;
|
|
2510
|
+
case 'rules':
|
|
2511
|
+
run(cmdTeamRules);
|
|
2512
|
+
break;
|
|
2103
2513
|
default:
|
|
2104
2514
|
showTeamHelp();
|
|
2105
2515
|
break;
|