kitty-hive 0.6.12 → 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/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 +26 -0
- package/dist/db.js +63 -2
- package/dist/db.js.map +1 -1
- package/dist/index.js +199 -3
- 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/task-tools.js +30 -10
- package/dist/mcp/task-tools.js.map +1 -1
- package/dist/models.d.ts +2 -0
- package/dist/models.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/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -86,6 +86,115 @@ async function cmdCodexChannel() {
|
|
|
86
86
|
// Block until child exits (handled above)
|
|
87
87
|
await new Promise(() => { });
|
|
88
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
|
+
}
|
|
89
198
|
const INIT_TOOLS = ['claude', 'cursor', 'vscode'];
|
|
90
199
|
// codex is config'd via its own CLI (`codex mcp add ... --url ...`) writing
|
|
91
200
|
// to ~/.codex/config.toml — we shell out instead of editing toml ourselves.
|
|
@@ -397,6 +506,36 @@ async function cmdStatus() {
|
|
|
397
506
|
console.log(renderTable(['ID', 'NAME', 'PEER', 'STATUS', 'LAST SEEN'], rrows, ' '));
|
|
398
507
|
}
|
|
399
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 */ }
|
|
400
539
|
}
|
|
401
540
|
catch {
|
|
402
541
|
console.log(`\n⚠️ Cannot read database`);
|
|
@@ -425,7 +564,7 @@ async function cmdDbClear() {
|
|
|
425
564
|
console.log('✅ Database cleared.');
|
|
426
565
|
}
|
|
427
566
|
async function cmdAgentRemove() {
|
|
428
|
-
const { dbPath } = parseFlags(1);
|
|
567
|
+
const { dbPath, port } = parseFlags(1);
|
|
429
568
|
// Flags: --key <K> look up by external_key (idempotent: missing = exit 0)
|
|
430
569
|
// --yes skip confirmation (scripts)
|
|
431
570
|
// --transfer-to <a> transfer hosted teams to this agent before deleting
|
|
@@ -635,6 +774,27 @@ async function cmdAgentRemove() {
|
|
|
635
774
|
db.prepare('DELETE FROM teams WHERE host_agent_id = ?').run(agent.id);
|
|
636
775
|
db.prepare('DELETE FROM agents WHERE id = ?').run(agent.id);
|
|
637
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
|
+
}
|
|
638
798
|
}
|
|
639
799
|
async function cmdAgentRegister() {
|
|
640
800
|
// Idempotent upsert by external_key. Designed for `spawn('kitty-hive',
|
|
@@ -647,6 +807,7 @@ async function cmdAgentRegister() {
|
|
|
647
807
|
let roles = '';
|
|
648
808
|
let tool = '';
|
|
649
809
|
let agentIdOverride = '';
|
|
810
|
+
let projectDir = '';
|
|
650
811
|
for (let i = 2; i < args.length; i++) {
|
|
651
812
|
if (args[i] === '--key' && args[i + 1]) {
|
|
652
813
|
externalKey = args[i + 1];
|
|
@@ -672,12 +833,18 @@ async function cmdAgentRegister() {
|
|
|
672
833
|
agentIdOverride = args[i + 1];
|
|
673
834
|
i++;
|
|
674
835
|
}
|
|
836
|
+
else if (args[i] === '--project-dir' && args[i + 1]) {
|
|
837
|
+
projectDir = args[i + 1];
|
|
838
|
+
i++;
|
|
839
|
+
}
|
|
675
840
|
}
|
|
676
841
|
if (!externalKey && !agentIdOverride && !displayName) {
|
|
677
|
-
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]');
|
|
678
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).');
|
|
679
845
|
process.exit(1);
|
|
680
846
|
}
|
|
847
|
+
const { port } = parseFlags(1);
|
|
681
848
|
initDB(dbPath);
|
|
682
849
|
const { handleStart } = await import('./tools/start.js');
|
|
683
850
|
try {
|
|
@@ -687,11 +854,37 @@ async function cmdAgentRegister() {
|
|
|
687
854
|
name: displayName || undefined,
|
|
688
855
|
roles: roles || undefined,
|
|
689
856
|
tool: tool || undefined,
|
|
857
|
+
projectDir: projectDir || undefined,
|
|
690
858
|
});
|
|
691
859
|
// Stdout: one line, just the agent_id (script-friendly)
|
|
692
860
|
console.log(result.agent_id);
|
|
693
861
|
// Stderr: human context
|
|
694
|
-
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
|
+
}
|
|
695
888
|
}
|
|
696
889
|
catch (err) {
|
|
697
890
|
console.error(`Failed: ${err.message ?? err}`);
|
|
@@ -2278,6 +2471,9 @@ switch (command) {
|
|
|
2278
2471
|
case 'codex-channel':
|
|
2279
2472
|
run(cmdCodexChannel);
|
|
2280
2473
|
break;
|
|
2474
|
+
case 'codex-pane':
|
|
2475
|
+
run(cmdCodexPane);
|
|
2476
|
+
break;
|
|
2281
2477
|
case 'status':
|
|
2282
2478
|
run(cmdStatus);
|
|
2283
2479
|
break;
|